[TASK] Implement standalone functional test API 17/23117/13
authorHelmut Hummel <helmut.hummel@typo3.org>
Thu, 25 Jul 2013 08:33:17 +0000 (10:33 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Fri, 16 Aug 2013 20:15:08 +0000 (22:15 +0200)
Test encapsulation and a controlled environment is
crucial for solid functional tests.

The patch creates a full TYPO3 CMS instance within
typo3temp/ together with an own database and
LocalConfiguration to run a specific functional test
in this environment. A full TYPO3 CMS bootstrap of
this instance is done. A new environment with a fresh
PHP process is created for each and every single test.

Functional test can use the API by calling parent::setUp()
and parent::tearDown().

The functional suite can be called directly with phpunit
"./typo3conf/ext/phpunit/Composer/vendor/bin/phpunit
-c typo3/sysext/core/Build/FunctionalTests.xml"

Currently the test suite must be called from the document
root folder.

The patch is currently a base patch for the main API implementing
immediatly needed stuff. With further patches sanitizing and
more details will be added.

Change-Id: I54f652f6a346a5155b5c33e4a065ab37898ff5b2
Resolves: #51091
Releases: 6.2
Reviewed-on: https://review.typo3.org/23117
Reviewed-by: Christian Kuhn
Tested-by: Christian Kuhn
Reviewed-by: Stefan Neufeind
Reviewed-by: Tymoteusz Motylewski
Tested-by: Tymoteusz Motylewski
Reviewed-by: Anja Leichsenring
Tested-by: Anja Leichsenring
typo3/sysext/core/Build/FunctionalTests.xml
typo3/sysext/core/Build/FunctionalTestsBootstrap.php
typo3/sysext/core/Tests/Exception.php [new file with mode: 0644]
typo3/sysext/core/Tests/FunctionalTestCase.php
typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceTest.php

index 22b48c5..7b7dfc0 100644 (file)
@@ -6,7 +6,7 @@
        convertErrorsToExceptions="true"
        convertWarningsToExceptions="true"
        forceCoversAnnotation="false"
-       processIsolation="false"
+       processIsolation="true"
        stopOnError="false"
        stopOnFailure="false"
        stopOnIncomplete="false"
index 756ddef..838fdcf 100644 (file)
@@ -27,4 +27,8 @@
  */
 require_once(__DIR__ . '/../Tests/BaseTestCase.php');
 require_once(__DIR__ . '/../Tests/FunctionalTestCase.php');
+require_once(__DIR__ . '/../Tests/Exception.php');
+if (!defined('ORIGINAL_ROOT')) {
+       define('ORIGINAL_ROOT', $_SERVER['PWD']);
+}
 ?>
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Exception.php b/typo3/sysext/core/Tests/Exception.php
new file mode 100644 (file)
index 0000000..d100b0d
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace TYPO3\CMS\Core\Tests;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2013 Christian Kuhn <lolli@schwarzbu.ch>
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+/**
+ * An exception - Thrown in abstract test cases to mark
+ * a test configuration or setup error.
+ */
+class Exception extends \Exception {
+}
+?>
\ No newline at end of file
index 5d0a429..d11ff9a 100644 (file)
@@ -24,16 +24,470 @@ namespace TYPO3\CMS\Core\Tests;
  * This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
 /**
  * Base test case for functional tests.
  *
- * This class currently only inherits the base test case. However, it is recommended
- * to extend this class for unit test cases instead of the base test case because if,
- * at some point, specific behavior needs to be implemented for unit tests, your test cases
- * will profit from it automatically.
- *
+ * Functional tests should extend this class. It provides methods to create
+ * a new database with base data and methods to fiddle with test data.
  */
 abstract class FunctionalTestCase extends BaseTestCase {
 
+       /**
+        * @var string Name of test database - Private since test cases must not fiddle with this!
+        */
+       private $testDatabaseName;
+
+       /**
+        * Array of core extension names this test depends on
+        *
+        * @var array
+        */
+       protected $requiredExtensions = array();
+
+       /**
+        * Array of test/fixture extension names this test depends on
+        *
+        * @var array
+        */
+       protected $requiredTestExtensions = array();
+
+       /**
+        * Absolute path to the test installation root folder
+        *
+        * @var string
+        */
+       private $testInstallationPath;
+
+       /**
+        * Set up creates a test database and fills with data.
+        *
+        * This method should be called with parent::setUp() in your test cases!
+        *
+        * @return void
+        */
+       public function setUp() {
+               $this->calculateTestInstallationPath();
+               $this->setUpTestInstallationFolderStructure();
+               $this->copyMultipleTestExtensionsToExtFolder($this->requiredTestExtensions);
+               $this->setUpLocalConfiguration();
+               $this->setUpBasicTypo3Bootstrap();
+               $this->setUpTestDatabaseConnection();
+               $this->createDatabaseStructure();
+               \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(TRUE);
+       }
+
+       /**
+        * Tear down.
+        *
+        * This method should be called with parent::setUp() in your test cases!
+        *
+        * @throws \TYPO3\CMS\Core\Tests\Exception
+        * @return void
+        */
+       public function tearDown() {
+               if (empty($this->testDatabaseName)) {
+                       throw new Exception(
+                               'Test database name not set. parent::setUp called?',
+                               1376579421
+                       );
+               }
+               $this->tearDownTestDatabase();
+               $this->tearDownTestInstallationFolder();
+       }
+
+       /**
+        * Calculates path to the test TYPO3 installation
+        *
+        * @return void
+        */
+       private function calculateTestInstallationPath() {
+               // @TODO: same id for filesystem & database name
+               $this->testInstallationPath = ORIGINAL_ROOT . '/typo3temp/'. uniqid('functional');
+       }
+
+       /**
+        * Calculates test database name based on original database name
+        *
+        * @param string $originalDatabaseName Name of original database
+        * @return void
+        */
+       private function calculateTestDatabaseName($originalDatabaseName) {
+               // @TODO: same id for filesystem & database name
+               $this->testDatabaseName = uniqid(strtolower($originalDatabaseName . '_test_'));
+       }
+
+       /**
+        * Creates folder structure of the test installation and link TYPO3 core
+        *
+        * @throws Exception
+        * @return void
+        */
+       private function setUpTestInstallationFolderStructure() {
+               $neededFolders = array(
+                       '',
+                       '/fileadmin',
+                       '/typo3temp',
+                       '/typo3conf',
+                       '/typo3conf/ext',
+                       '/uploads'
+               );
+               foreach ($neededFolders as $folder) {
+                       $success = mkdir($this->testInstallationPath . $folder);
+                       if (!$success) {
+                               throw new Exception('Can not create directory: ' . $this->testInstallationPath . $folder, 1376657189);
+                       }
+               }
+
+               $neededLinks = array(
+                       '/typo3' => '/typo3',
+                       '/index.php' => '/index.php'
+               );
+               foreach ($neededLinks as $from => $to) {
+                       $success = symlink(ORIGINAL_ROOT . $from, $this->testInstallationPath . $to);
+                       if (!$success) {
+                               throw new Exception('Can not link file : ' . ORIGINAL_ROOT . $from . ' to: ' . $this->testInstallationPath . $to, 1376657199);
+                       }
+               }
+       }
+
+       /**
+        * Create new $GLOBALS['TYPO3_DB'] on test database
+        *
+        * @throws \TYPO3\CMS\Core\Tests\Exception
+        * @return void
+        */
+       private function setUpTestDatabaseConnection() {
+               \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeTypo3DbGlobal();
+               $GLOBALS['TYPO3_DB']->sql_pconnect();
+               $createDatabaseResult = $GLOBALS['TYPO3_DB']->admin_query('CREATE DATABASE `' . $this->testDatabaseName . '`');
+               if (!$createDatabaseResult) {
+                       throw new Exception(
+                               'Unable to create database with name ' . $this->testDatabaseName . ' permission problem?',
+                               1376579070
+                       );
+               }
+               $GLOBALS['TYPO3_DB']->setDatabaseName($this->testDatabaseName);
+               $GLOBALS['TYPO3_DB']->sql_select_db($this->testDatabaseName);
+       }
+
+       /**
+        * Creates LocalConfiguration.php file in the test installation
+        *
+        * @return void
+        */
+       private function setUpLocalConfiguration() {
+               $localConfigurationFile = $this->testInstallationPath . '/typo3conf/LocalConfiguration.php';
+               $originalConfigurationArray = require ORIGINAL_ROOT . '/typo3conf/LocalConfiguration.php';
+               $localConfigurationArray = require ORIGINAL_ROOT .'/typo3/sysext/core/Configuration/FactoryConfiguration.php';
+
+
+               $additionalConfiguration = array('DB' => $originalConfigurationArray['DB']);
+               $this->calculateTestDatabaseName($additionalConfiguration['DB']['database']);
+               $additionalConfiguration['DB']['database'] = $this->testDatabaseName;
+               $localConfigurationArray['DB'] = $additionalConfiguration['DB'];
+
+               $extensions = array_merge($this->requiredExtensions, $this->requiredTestExtensions);
+               $localConfigurationArray['EXT']['extListArray'] = $extensions;
+
+               $result = $this->writeFile(
+                       $localConfigurationFile,
+                       '<?php' . chr(10) .
+                       'return ' .
+                       $this->arrayExport(
+                               $localConfigurationArray
+                       ) .
+                       ';' . chr(10) .
+                       '?>'
+               );
+               if (!$result) {
+                       throw new Exception('Can not write local configuration', 1376657277);
+               }
+       }
+
+       /**
+        * Bootstrap basic TYPO3
+        *
+        * @return void
+        */
+       private function setUpBasicTypo3Bootstrap() {
+               $_SERVER['PWD'] = $this->testInstallationPath;
+               $_SERVER['argv'][0] = 'index.php';
+
+               define('TYPO3_MODE', 'BE');
+               define('TYPO3_cliMode', TRUE);
+
+               require $this->testInstallationPath . '/typo3/sysext/core/Classes/Core/CliBootstrap.php';
+               \TYPO3\CMS\Core\Core\CliBootstrap::checkEnvironmentOrDie();
+
+               require $this->testInstallationPath . '/typo3/sysext/core/Classes/Core/Bootstrap.php';
+               \TYPO3\CMS\Core\Core\Bootstrap::getInstance()
+                       ->baseSetup('')
+                       ->loadConfigurationAndInitialize(FALSE)
+                       ->loadTypo3LoadedExtAndExtLocalconf(FALSE)
+                       ->applyAdditionalConfigurationSettings();
+       }
+
+       /**
+        * Drop the test database.
+        *
+        * @throws \TYPO3\CMS\Core\Tests\Exception
+        * @return void
+        */
+       private function tearDownTestDatabase() {
+               $result = $GLOBALS['TYPO3_DB']->admin_query('DROP DATABASE `' . $this->testDatabaseName . '`');
+               if (!$result) {
+                       throw new Exception(
+                               'Dropping test database ' . $this->testDatabaseName . ' failed',
+                               1376583188
+                       );
+               }
+       }
+
+       /**
+        * Removes test installation folder
+        *
+        * @throws \TYPO3\CMS\Core\Tests\Exception
+        * @return void
+        */
+       private function tearDownTestInstallationFolder() {
+               $success = $this->rmdir($this->testInstallationPath, TRUE);
+               if (!$success) {
+                       throw new Exception('Can not remove folder: ' . $this->testInstallationPath, 1376657210);
+               }
+       }
+
+       /**
+        * Create tables and import static rows
+        *
+        * @return void
+        */
+       private function createDatabaseStructure() {
+               /** @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService */
+               $schemaMigrationService = GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlSchemaMigrationService');
+               /** @var \TYPO3\CMS\Install\Service\SqlExpectedSchemaService $expectedSchemaService */
+               $expectedSchemaService = GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlExpectedSchemaService');
+
+               // Raw concatenated ext_tables.sql and friends string
+               $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(TRUE);
+               $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, TRUE);
+               list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, TRUE);
+
+               $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
+               $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
+               $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
+               $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
+
+               $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
+               $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
+               $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
+
+               foreach ($insertCount as $table => $count) {
+                       $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
+                       foreach ($insertStatements as $insertQuery) {
+                               $insertQuery = rtrim($insertQuery, ';');
+                               $GLOBALS['TYPO3_DB']->admin_query($insertQuery);
+                       }
+               }
+       }
+
+       /**
+        * Copy all needed test extensions to the typo3conf/ext folder of the test installation
+        *
+        * @param array $extensionNames array containing extension names (name should be the same as a folder name)
+        * @return void
+        */
+       private function copyMultipleTestExtensionsToExtFolder(array $extensionNames) {
+               foreach ($extensionNames as $extensionName) {
+                       $extensionPath = $this->getFixtureExtensionPath($extensionName);
+                       $this->copyTestExtensionToExtFolder($extensionPath);
+               }
+       }
+
+       /**
+        * Copy single single test extension to the typo3conf/ext folder of the test installation
+        *
+        * @param string $sourceFolderPath absolute path to extension
+        * @throws \TYPO3\CMS\Core\Tests\Exception
+        * @return void
+        */
+       private function copyTestExtensionToExtFolder($sourceFolderPath) {
+               if (!stristr(PHP_OS, 'darwin') && stristr(PHP_OS, 'win')) {
+                       // Windows
+                       $sourceFolderPath = rtrim($sourceFolderPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+                       $files = GeneralUtility::getAllFilesAndFoldersInPath(array(), $sourceFolderPath, '', TRUE);
+                       $files = GeneralUtility::removePrefixPathFromList($files, $sourceFolderPath);
+
+                       foreach ($files as $fileName) {
+                               $destinationPath = $this->testInstallationPath . DIRECTORY_SEPARATOR . 'typo3conf' . DIRECTORY_SEPARATOR . 'ext'. DIRECTORY_SEPARATOR . $fileName;
+                               $success = copy($sourceFolderPath . $fileName, $destinationPath);
+                               if (!$success) {
+                                       throw new Exception('Can not copy file: ' . $fileName . ' to ' . $destinationPath, 1376657187);
+                               }
+                       }
+               } else {
+                       //linux
+                       $destinationPath = $this->testInstallationPath . DIRECTORY_SEPARATOR . 'typo3conf' . DIRECTORY_SEPARATOR . 'ext'. DIRECTORY_SEPARATOR. basename($sourceFolderPath);
+                       $success = symlink($sourceFolderPath, $destinationPath);
+                       if (!$success) {
+                               throw new Exception('Can not link folder: ' . $sourceFolderPath . ' to ' . $destinationPath, 1376657187);
+                       }
+               }
+       }
+
+       /**
+        * Returns absolute path to the fixture
+        * if called with empty $relativeFixturePath, returns path to the base folder for fixtures
+        *
+        * @param string $relativeFixturePath
+        * @return string absolute path with trailing slash
+        * @TODO: Figure out if this is useful
+        */
+       protected function getFixturePath($relativeFixturePath = '') {
+               $relativeFixturePath = !empty($relativeFixturePath) ? $relativeFixturePath . DIRECTORY_SEPARATOR : '';
+               $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR . $relativeFixturePath;
+               return $path;
+       }
+
+       /**
+        * Returns absolute path to the fixture extension
+        * if called with empty name, returns path to the base folder for test extensions
+        *
+        * @param string $name
+        * @return string absolute path with trailing slash
+        * @TODO: Figure out if this is useful
+        */
+       protected function getFixtureExtensionPath($name = '') {
+               $name = !empty($name) ? $name . DIRECTORY_SEPARATOR : '';
+               $path = $this->getFixturePath() . 'extensions' . DIRECTORY_SEPARATOR . $name;
+               return $path;
+       }
+
+       /**
+        * METHODS COPIED FROM GeneralUtility
+        */
+
+       /**
+        * COPIED FROM GeneralUtility
+        *
+        * Wrapper function for rmdir, allowing recursive deletion of folders and files
+        *
+        * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
+        * @param boolean $removeNonEmpty Allow deletion of non-empty directories
+        * @return boolean TRUE if @rmdir went well!
+        */
+       private function rmdir($path, $removeNonEmpty = FALSE) {
+               $OK = FALSE;
+               // Remove trailing slash
+               $path = preg_replace('|/$|', '', $path);
+               if (file_exists($path)) {
+                       $OK = TRUE;
+                       if (!is_link($path) && is_dir($path)) {
+                               if ($removeNonEmpty == TRUE && ($handle = opendir($path))) {
+                                       while ($OK && FALSE !== ($file = readdir($handle))) {
+                                               if ($file == '.' || $file == '..') {
+                                                       continue;
+                                               }
+                                               $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
+                                       }
+                                       closedir($handle);
+                               }
+                               if ($OK) {
+                                       $OK = @rmdir($path);
+                               }
+                       } else {
+                               // If $path is a file, simply remove it
+                               $OK = unlink($path);
+                       }
+                       clearstatcache();
+               } elseif (is_link($path)) {
+                       $OK = unlink($path);
+                       clearstatcache();
+               }
+               return $OK;
+       }
+
+       /**
+        * Writes $content to the file $file
+        *
+        * @param string $file Filepath to write to
+        * @param string $content Content to write
+        * @return boolean TRUE if the file was successfully opened and written to.
+        */
+       private function writeFile($file, $content) {
+               if ($fd = fopen($file, 'wb')) {
+                       $res = fwrite($fd, $content);
+                       fclose($fd);
+                       if ($res === FALSE) {
+                               return FALSE;
+                       }
+                       return TRUE;
+               }
+               return FALSE;
+       }
+
+       /**
+        * METHODS COPIED FROM ArrayUtility
+        */
+
+       /**
+        * Exports an array as string.
+        * Similar to var_export(), but representation follows the TYPO3 core CGL.
+        *
+        * See unit tests for detailed examples
+        *
+        * @param array $array Array to export
+        * @param integer $level Internal level used for recursion, do *not* set from outside!
+        * @return string String representation of array
+        * @throws \RuntimeException
+        */
+       private function arrayExport(array $array = array(), $level = 0) {
+               $lines = 'array(' . chr(10);
+               $level++;
+               $writeKeyIndex = FALSE;
+               $expectedKeyIndex = 0;
+               foreach ($array as $key => $value) {
+                       if ($key === $expectedKeyIndex) {
+                               $expectedKeyIndex++;
+                       } else {
+                               // Found a non integer or non consecutive key, so we can break here
+                               $writeKeyIndex = TRUE;
+                               break;
+                       }
+               }
+               foreach ($array as $key => $value) {
+                       // Indention
+                       $lines .= str_repeat(chr(9), $level);
+                       if ($writeKeyIndex) {
+                               // Numeric / string keys
+                               $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
+                       }
+                       if (is_array($value)) {
+                               if (count($value) > 0) {
+                                       $lines .= $this->arrayExport($value, $level);
+                               } else {
+                                       $lines .= 'array(),' . chr(10);
+                               }
+                       } elseif (is_int($value) || is_float($value)) {
+                               $lines .= $value . ',' . chr(10);
+                       } elseif (is_null($value)) {
+                               $lines .= 'NULL' . ',' . chr(10);
+                       } elseif (is_bool($value)) {
+                               $lines .= $value ? 'TRUE' : 'FALSE';
+                               $lines .= ',' . chr(10);
+                       } elseif (is_string($value)) {
+                               // Quote \ to \\
+                               $stringContent = str_replace('\\', '\\\\', $value);
+                               // Quote ' to \'
+                               $stringContent = str_replace('\'', '\\\'', $stringContent);
+                               $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
+                       } else {
+                               throw new \RuntimeException('Objects are not supported', 1342294986);
+                       }
+               }
+               $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10));
+               return $lines;
+       }
 }
 ?>
\ No newline at end of file
index 442263a..45e4b12 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-namespace TYPO3\CMS\Workspaces\Service;
+namespace TYPO3\CMS\Workspaces\Tests\Functional\Service;
 
 /***************************************************************
  *  Copyright notice
@@ -194,4 +194,4 @@ class WorkspacesServiceTest extends \tx_phpunit_database_testcase {
 }
 
 
-?>
\ No newline at end of file
+?>