[TASK] Acceptance tests in controlled environment
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Testbase.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Core\Bootstrap;
18 use TYPO3\CMS\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Extbase\Object\ObjectManager;
21 use TYPO3\CMS\Install\Service\SqlExpectedSchemaService;
22 use TYPO3\CMS\Install\Service\SqlSchemaMigrationService;
23
24 /**
25 * This is a helper class used by unit, functional and acceptance test
26 * environment builders.
27 * It contains methods to create test environments.
28 *
29 * This class is for internal use only and may change wihtout further notice.
30 *
31 * Use the classes "UnitTestCase", "FunctionalTestCase" or "AcceptanceCoreEnvironment"
32 * to indirectly benefit from this class in own extensions.
33 */
34 class Testbase {
35
36 /**
37 * Makes sure error messages during the tests get displayed no matter what is set in php.ini.
38 *
39 * @return void
40 */
41 public function enableDisplayErrors()
42 {
43 @ini_set('display_errors', 1);
44 }
45
46 /**
47 * Defines a list of basic constants that are used by GeneralUtility and other
48 * helpers during tests setup. Those are sanitized in SystemEnvironmentBuilder
49 * to be not defined again.
50 *
51 * @return void
52 * @see SystemEnvironmentBuilder::defineBaseConstants()
53 */
54 public function defineBaseConstants()
55 {
56 // A null, a tabulator, a linefeed, a carriage return, a substitution, a CR-LF combination
57 defined('NUL') ?: define('NUL', chr(0));
58 defined('TAB') ?: define('TAB', chr(9));
59 defined('LF') ?: define('LF', chr(10));
60 defined('CR') ?: define('CR', chr(13));
61 defined('SUB') ?: define('SUB', chr(26));
62 defined('CRLF') ?: define('CRLF', CR . LF);
63
64 if (!defined('TYPO3_OS')) {
65 // Operating system identifier
66 // Either "WIN" or empty string
67 $typoOs = '';
68 if (!stristr(PHP_OS, 'darwin') && !stristr(PHP_OS, 'cygwin') && stristr(PHP_OS, 'win')) {
69 $typoOs = 'WIN';
70 }
71 define('TYPO3_OS', $typoOs);
72 }
73 }
74
75 /**
76 * Defines the PATH_site and PATH_thisScript constant and sets $_SERVER['SCRIPT_NAME'].
77 * For unit tests only
78 *
79 * @return void
80 */
81 public function defineSitePath()
82 {
83 /** @var string */
84 define('PATH_site', $this->getWebRoot());
85 /** @var string */
86 define('PATH_thisScript', PATH_site . 'typo3/cli_dispatch.phpsh');
87 $_SERVER['SCRIPT_NAME'] = PATH_thisScript;
88
89 if (!file_exists(PATH_thisScript)) {
90 die('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
91 }
92 }
93
94 /**
95 * Defines the constant ORIGINAL_ROOT for the path to the original TYPO3 document root.
96 * For functional / acceptance tests only
97 * If ORIGINAL_ROOT already is defined, this method is a no-op.
98 *
99 * @return void
100 */
101 public function defineOriginalRootPath()
102 {
103 if (!defined('ORIGINAL_ROOT')) {
104 /** @var string */
105 define('ORIGINAL_ROOT', $this->getWebRoot());
106 }
107
108 if (!file_exists(ORIGINAL_ROOT . 'typo3/cli_dispatch.phpsh')) {
109 die('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
110 }
111 }
112
113 /**
114 * Define TYPO3_MODE to BE
115 *
116 * @return void
117 */
118 public function defineTypo3ModeBe()
119 {
120 /** @var string */
121 define('TYPO3_MODE', 'BE');
122 }
123
124 /**
125 * Sets the environment variable TYPO3_CONTEXT to testing.
126 *
127 * @return void
128 */
129 public function setTypo3TestingContext()
130 {
131 /** @var string */
132 putenv('TYPO3_CONTEXT=Testing');
133 }
134
135 /**
136 * Creates directories, recursively if required.
137 *
138 * @param string $directory Absolute path to directories to create
139 * @return void
140 * @throws Exception
141 */
142 public function createDirectory($directory)
143 {
144 if (is_dir($directory)) {
145 return;
146 }
147 @mkdir($directory, 0777, true);
148 clearstatcache();
149 if (!is_dir($directory)) {
150 throw new Exception('Directory "' . $directory . '" could not be created', 1404038665);
151 }
152 }
153
154 /**
155 * Checks whether given test instance exists in path and is younger than some minutes.
156 * Used in functional tests
157 *
158 * @param string $instancePath Absolute path to test instance
159 * @return bool
160 */
161 public function recentTestInstanceExists($instancePath)
162 {
163 if (@file_get_contents($instancePath . '/last_run.txt') <= (time() - 300)) {
164 return false;
165 } else {
166 // Test instance exists and is pretty young -> re-use
167 return true;
168 }
169 }
170
171 /**
172 * Remove test instance folder structure if it exists.
173 * This may happen if a functional test before threw a fatal or is too old
174 *
175 * @param string $instancePath Absolute path to test instance
176 * @return void
177 * @throws Exception
178 */
179 public function removeOldInstanceIfExists($instancePath)
180 {
181 if (is_dir($instancePath)) {
182 $success = GeneralUtility::rmdir($instancePath, true);
183 if (!$success) {
184 throw new Exception(
185 'Can not remove folder: ' . $instancePath,
186 1376657210
187 );
188 }
189 }
190 }
191
192 /**
193 * Create last_run.txt file within instance path containing timestamp of "now".
194 * Used in functional tests to reuse an instance for multiple tests in one test case.
195 *
196 * @param string $instancePath Absolute path to test instance
197 * @return void
198 */
199 public function createLastRunTextfile($instancePath)
200 {
201 // Store the time instance was created
202 file_put_contents($instancePath . '/last_run.txt', time());
203 }
204
205 /**
206 * Link TYPO3 CMS core from "parent" instance.
207 * For functional and acceptance tests.
208 *
209 * @param string $instancePath Absolute path to test instance
210 * @throws Exception
211 * @return void
212 */
213 public function setUpInstanceCoreLinks($instancePath)
214 {
215 $linksToSet = array(
216 ORIGINAL_ROOT . 'typo3' => $instancePath . '/typo3',
217 ORIGINAL_ROOT . 'index.php' => $instancePath . '/index.php'
218 );
219 foreach ($linksToSet as $from => $to) {
220 $success = symlink($from, $to);
221 if (!$success) {
222 throw new Exception(
223 'Creating link failed: from ' . $from . ' to: ' . $to,
224 1376657199
225 );
226 }
227 }
228 }
229
230 /**
231 * Link test extensions to the typo3conf/ext folder of the instance.
232 * For functional and acceptance tests.
233 *
234 * @param string $instancePath Absolute path to test instance
235 * @param array $extensionPaths Contains paths to extensions relative to document root
236 * @throws Exception
237 * @return void
238 */
239 public function linkTestExtensionsToInstance($instancePath, array $extensionPaths)
240 {
241 foreach ($extensionPaths as $extensionPath) {
242 $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
243 if (!is_dir($absoluteExtensionPath)) {
244 throw new Exception(
245 'Test extension path ' . $absoluteExtensionPath . ' not found',
246 1376745645
247 );
248 }
249 $destinationPath = $instancePath . '/typo3conf/ext/' . basename($absoluteExtensionPath);
250 $success = symlink($absoluteExtensionPath, $destinationPath);
251 if (!$success) {
252 throw new Exception(
253 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
254 1376657142
255 );
256 }
257 }
258 }
259
260 /**
261 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
262 * test instance fileadmin folder.
263 * For functional and acceptance tests.
264 *
265 * @param string $instancePath Absolute path to test instance
266 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
267 * @throws Exception if a source path could not be found and on failing creating the symlink
268 * @return void
269 */
270 public function linkPathsInTestInstance($instancePath, array $pathsToLinkInTestInstance)
271 {
272 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
273 $sourcePath = $instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
274 if (!file_exists($sourcePath)) {
275 throw new Exception(
276 'Path ' . $sourcePath . ' not found',
277 1376745645
278 );
279 }
280 $destinationPath = $instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
281 $success = symlink($sourcePath, $destinationPath);
282 if (!$success) {
283 throw new Exception(
284 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
285 1389969623
286 );
287 }
288 }
289 }
290
291 /**
292 * Database settings for functional and acceptance tests can be either set by
293 * environment variables (recommended), or from an existing LocalConfiguration as fallback.
294 * The method fetches these.
295 *
296 * An unique name will be added to the database name later.
297 *
298 * @throws Exception
299 * @return array [DB][host], [DB][username], ...
300 */
301 public function getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration()
302 {
303 $databaseName = trim(getenv('typo3DatabaseName'));
304 $databaseHost = trim(getenv('typo3DatabaseHost'));
305 $databaseUsername = trim(getenv('typo3DatabaseUsername'));
306 $databasePassword = trim(getenv('typo3DatabasePassword'));
307 $databasePort = trim(getenv('typo3DatabasePort'));
308 $databaseSocket = trim(getenv('typo3DatabaseSocket'));
309 if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) {
310 // Try to get database credentials from environment variables first
311 $originalConfigurationArray = array(
312 'DB' => array(),
313 );
314 if ($databaseName) {
315 $originalConfigurationArray['DB']['database'] = $databaseName;
316 }
317 if ($databaseHost) {
318 $originalConfigurationArray['DB']['host'] = $databaseHost;
319 }
320 if ($databaseUsername) {
321 $originalConfigurationArray['DB']['username'] = $databaseUsername;
322 }
323 if ($databasePassword) {
324 $originalConfigurationArray['DB']['password'] = $databasePassword;
325 }
326 if ($databasePort) {
327 $originalConfigurationArray['DB']['port'] = $databasePort;
328 }
329 if ($databaseSocket) {
330 $originalConfigurationArray['DB']['socket'] = $databaseSocket;
331 }
332 } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) {
333 // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
334 $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
335 } else {
336 throw new Exception(
337 'Database credentials for tests are neither set through environment'
338 . ' variables, and can not be found in an existing LocalConfiguration file',
339 1397406356
340 );
341 }
342 return $originalConfigurationArray;
343 }
344
345 /**
346 * Maximum length of database names is 64 chars in mysql. Test this is not exceeded
347 * after a suffix has been added.
348 *
349 * @param string $originalDatabaseName Base name of the database
350 * @param array $configuration "LocalConfiguration" array with DB settings
351 * @throws Exception
352 */
353 public function testDatabaseNameIsNotTooLong($originalDatabaseName, array $configuration)
354 {
355 // Maximum database name length for mysql is 64 characters
356 if (strlen($configuration['DB']['database']) > 64) {
357 $suffixLength = strlen($configuration['DB']['database']) - strlen($originalDatabaseName);
358 $maximumOriginalDatabaseName = 64 - $suffixLength;
359 throw new Exception(
360 'The name of the database that is used for the functional test (' . $originalDatabaseName . ')' .
361 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
362 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
363 1377600104
364 );
365 }
366 }
367
368 /**
369 * Create LocalConfiguration.php file of the test instance.
370 * For functional and acceptance tests.
371 *
372 * @param string $instancePath Absolute path to test instance
373 * @param array $configuration Base configuration array
374 * @param array $overruleConfiguration Overrule factory and base configuration
375 * @throws Exception
376 * @return void
377 */
378 public function setUpLocalConfiguration($instancePath, array $configuration, array $overruleConfiguration)
379 {
380 // Base of final LocalConfiguration is core factory configuration
381 $finalConfigurationArray = require ORIGINAL_ROOT . 'typo3/sysext/core/Configuration/FactoryConfiguration.php';
382 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $configuration);
383 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $overruleConfiguration);
384 $result = $this->writeFile(
385 $instancePath . '/typo3conf/LocalConfiguration.php',
386 '<?php' . chr(10) .
387 'return ' .
388 ArrayUtility::arrayExport(
389 $finalConfigurationArray
390 ) .
391 ';'
392 );
393 if (!$result) {
394 throw new Exception('Can not write local configuration', 1376657277);
395 }
396 }
397
398 /**
399 * Compile typo3conf/PackageStates.php containing default packages like core,
400 * a test specific list of additional core extensions, and a list of
401 * test extensions.
402 * For functional and acceptance tests.
403 *
404 * @param string $instancePath Absolute path to test instance
405 * @param array $defaultCoreExtensionsToLoad Default list of core extensions to load
406 * @param array $additionalCoreExtensionsToLoad Additional core extensions to load
407 * @param array $testExtensionPaths Paths to extensions relative to document root
408 * @throws Exception
409 */
410 public function setUpPackageStates(
411 $instancePath,
412 array $defaultCoreExtensionsToLoad,
413 array $additionalCoreExtensionsToLoad,
414 array $testExtensionPaths
415 ) {
416 $packageStates = array(
417 'packages' => array(),
418 'version' => 4,
419 );
420
421 // Register default list of extensions and set active
422 foreach ($defaultCoreExtensionsToLoad as $extensionName) {
423 $packageStates['packages'][$extensionName] = array(
424 'state' => 'active',
425 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
426 'classesPath' => 'Classes/',
427 );
428 }
429
430 // Register additional core extensions and set active
431 foreach ($additionalCoreExtensionsToLoad as $extensionName) {
432 if (isset($packageSates['packages'][$extensionName])) {
433 throw new Exception(
434 $extensionName . ' is already registered as default core extension to load, no need to load it explicitly',
435 1390913893
436 );
437 }
438 $packageStates['packages'][$extensionName] = array(
439 'state' => 'active',
440 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
441 'classesPath' => 'Classes/',
442 );
443 }
444
445 // Activate test extensions that have been symlinked before
446 foreach ($testExtensionPaths as $extensionPath) {
447 $extensionName = basename($extensionPath);
448 if (isset($packageSates['packages'][$extensionName])) {
449 throw new Exception(
450 $extensionName . ' is already registered as extension to load, no need to load it explicitly',
451 1390913894
452 );
453 }
454 $packageStates['packages'][$extensionName] = array(
455 'state' => 'active',
456 'packagePath' => 'typo3conf/ext/' . $extensionName . '/',
457 'classesPath' => 'Classes/',
458 );
459 }
460
461 $result = $this->writeFile(
462 $instancePath . '/typo3conf/PackageStates.php',
463 '<?php' . chr(10) .
464 'return ' .
465 ArrayUtility::arrayExport(
466 $packageStates
467 ) .
468 ';'
469 );
470
471 if (!$result) {
472 throw new Exception('Can not write PackageStates', 1381612729);
473 }
474 }
475
476 /**
477 * Populate $GLOBALS['TYPO3_DB'] and create test database
478 * For functional and acceptance tests
479 *
480 * @param string $databaseName Database name of this test instance
481 * @param string $originalDatabaseName Original database name before suffix was added
482 * @throws \TYPO3\CMS\Core\Tests\Exception
483 * @return void
484 */
485 public function setUpTestDatabase($databaseName, $originalDatabaseName)
486 {
487 Bootstrap::getInstance()->initializeTypo3DbGlobal();
488 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
489 $database = $GLOBALS['TYPO3_DB'];
490 if (!$database->sql_pconnect()) {
491 throw new Exception(
492 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
493 . ' connection to the database was attempted to be established!',
494 1377620117
495 );
496 }
497
498 // Drop database if exists
499 $database->admin_query('DROP DATABASE IF EXISTS `' . $databaseName . '`');
500 $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $databaseName . '` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci');
501 if (!$createDatabaseResult) {
502 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
503 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
504 throw new Exception(
505 'Unable to create database with name ' . $databaseName . '. This is probably a permission problem.'
506 . ' For this instance this could be fixed executing'
507 . ' "GRANT ALL ON `' . $originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
508 1376579070
509 );
510 }
511 $database->setDatabaseName($databaseName);
512
513 // On windows, this still works, but throws a warning, which we need to discard.
514 @$database->sql_select_db();
515 }
516
517 /**
518 * Bootstrap basic TYPO3. This bootstraps TYPO3 far enough to initialize database afterwards.
519 * For functional and acceptance tests.
520 *
521 * @param string $instancePath Absolute path to test instance
522 * @return void
523 */
524 public function setUpBasicTypo3Bootstrap($instancePath)
525 {
526 $_SERVER['PWD'] = $instancePath;
527 $_SERVER['argv'][0] = 'index.php';
528
529 $classLoader = require rtrim(realpath($instancePath . '/typo3'), '\\/') . '/../vendor/autoload.php';
530 Bootstrap::getInstance()
531 ->initializeClassLoader($classLoader)
532 ->setRequestType(TYPO3_REQUESTTYPE_BE | TYPO3_REQUESTTYPE_CLI)
533 ->baseSetup('')
534 ->loadConfigurationAndInitialize(true)
535 ->loadTypo3LoadedExtAndExtLocalconf(true)
536 ->setFinalCachingFrameworkCacheConfiguration()
537 ->defineLoggingAndExceptionConstants()
538 ->unsetReservedGlobalVariables();
539 }
540
541 /**
542 * Populate $GLOBALS['TYPO3_DB'] and truncate all tables.
543 * For functional and acceptance tests.
544 *
545 * @throws Exception
546 * @return void
547 */
548 public function initializeTestDatabaseAndTruncateTables()
549 {
550 Bootstrap::getInstance()->initializeTypo3DbGlobal();
551 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
552 $database = $GLOBALS['TYPO3_DB'];
553 if (!$database->sql_pconnect()) {
554 throw new Exception(
555 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
556 . ' connection to the database was attempted to be established!',
557 1377620117
558 );
559 }
560 $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
561 $database->sql_select_db();
562 foreach ($database->admin_get_tables() as $table) {
563 $database->admin_query('TRUNCATE ' . $table['Name'] . ';');
564 }
565 }
566
567 /**
568 * Load ext_tables.php files.
569 * For functional and acceptance tests.
570 *
571 * @return void
572 */
573 public function loadExtensionTables()
574 {
575 Bootstrap::getInstance()->loadExtensionTables();
576 }
577
578 /**
579 * Create tables and import static rows.
580 * For functional and acceptance tests.
581 *
582 * @return void
583 */
584 public function createDatabaseStructure()
585 {
586 /** @var SqlSchemaMigrationService $schemaMigrationService */
587 $schemaMigrationService = GeneralUtility::makeInstance(SqlSchemaMigrationService::class);
588 /** @var ObjectManager $objectManager */
589 $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
590 /** @var SqlExpectedSchemaService $expectedSchemaService */
591 $expectedSchemaService = $objectManager->get(SqlExpectedSchemaService::class);
592
593 // Raw concatenated ext_tables.sql and friends string
594 $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(true);
595 $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, true);
596 list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, true);
597
598 $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
599 $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
600 $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
601 $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
602
603 $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
604 $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
605 $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
606
607 foreach ($insertCount as $table => $count) {
608 $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
609 foreach ($insertStatements as $insertQuery) {
610 $insertQuery = rtrim($insertQuery, ';');
611 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
612 $database = $GLOBALS['TYPO3_DB'];
613 $database->admin_query($insertQuery);
614 }
615 }
616 }
617
618 /**
619 * Returns the absolute path the TYPO3 document root.
620 * This is the "original" document root, not the "instance" root for functional / acceptance tests.
621 *
622 * @return string the TYPO3 document root using Unix path separators
623 */
624 protected function getWebRoot()
625 {
626 if (getenv('TYPO3_PATH_WEB')) {
627 $webRoot = getenv('TYPO3_PATH_WEB');
628 } else {
629 $webRoot = getcwd();
630 }
631 return rtrim(strtr($webRoot, '\\', '/'), '/') . '/';
632 }
633
634 /**
635 * Writes $content to the file $file. This is a simplified version
636 * of GeneralUtility::writeFile that does not fix permissions.
637 *
638 * @param string $file Filepath to write to
639 * @param string $content Content to write
640 * @return bool TRUE if the file was successfully opened and written to.
641 */
642 protected function writeFile($file, $content)
643 {
644 if ($fd = fopen($file, 'wb')) {
645 $res = fwrite($fd, $content);
646 fclose($fd);
647 if ($res === false) {
648 return false;
649 }
650 return true;
651 }
652 return false;
653 }
654 }