From d5821399fc9beff3cf442a64c7171ca7394655bf Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Tue, 18 Aug 2015 01:25:27 +0100 Subject: [PATCH] =?UTF-8?q?Issue=20#2304461=20by=20dawehner,=20neclimdul,?= =?UTF-8?q?=20naveenvalecha,=20jibran,=20tim.plunkett,=20phenaproxima,=20X?= =?UTF-8?q?ano,=20hussainweb,=20sun,=20hitesh-jain,=20amateescu,=20alexpot?= =?UTF-8?q?t,=20daffie,=20Mile23,=20Wim=20Leers,=20larowlan:=20KernelTestB?= =?UTF-8?q?aseTNG=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/includes/install.inc | 4 +- core/lib/Drupal/Core/Config/FileStorage.php | 28 +- .../lib/Drupal/Core/Config/InstallStorage.php | 26 +- core/lib/Drupal/Core/DrupalKernel.php | 38 +- .../lib/Drupal/Core/DrupalKernelInterface.php | 3 +- .../Core/PhpStorage/PhpStorageFactory.php | 2 +- .../Drupal/Core/StreamWrapper/LocalStream.php | 9 + .../ckeditor/src/Plugin/Editor/CKEditor.php | 10 +- .../modules/simpletest/src/KernelTestBase.php | 3 + .../simpletest/src/RandomGeneratorTrait.php | 131 ++ core/modules/simpletest/src/TestBase.php | 115 +- core/modules/simpletest/src/TestDiscovery.php | 2 + .../tests/src/Unit/TestBaseTest.php | 2 +- .../Kernel}/Extension/ModuleHandlerTest.php | 39 +- .../PhpStorage/PhpStorageFactoryTest.php | 18 +- core/phpunit.xml.dist | 10 + core/scripts/run-tests.sh | 2 +- .../Drupal/KernelTests/AssertLegacyTrait.php | 129 ++ .../Drupal/KernelTests/KernelTestBase.php | 1051 +++++++++++++++++ .../Drupal/KernelTests/KernelTestBaseTest.php | 219 ++++ core/tests/bootstrap.php | 50 +- 21 files changed, 1701 insertions(+), 190 deletions(-) create mode 100644 core/modules/simpletest/src/RandomGeneratorTrait.php rename core/modules/system/{src/Tests => tests/src/Kernel}/Extension/ModuleHandlerTest.php (91%) rename core/modules/system/{src/Tests => tests/src/Kernel}/PhpStorage/PhpStorageFactoryTest.php (86%) create mode 100644 core/tests/Drupal/KernelTests/AssertLegacyTrait.php create mode 100644 core/tests/Drupal/KernelTests/KernelTestBase.php create mode 100644 core/tests/Drupal/KernelTests/KernelTestBaseTest.php diff --git a/core/includes/install.inc b/core/includes/install.inc index 77e0b7090ab..f021373ba1b 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -210,7 +210,7 @@ function drupal_rewrite_settings($settings = array(), $settings_file = NULL) { } $variable_names['$'. $setting] = $setting; } - $contents = file_get_contents(DRUPAL_ROOT . '/' . $settings_file); + $contents = file_get_contents($settings_file); if ($contents !== FALSE) { // Initialize the contents for the settings.php file if it is empty. if (trim($contents) === '') { @@ -315,7 +315,7 @@ function drupal_rewrite_settings($settings = array(), $settings_file = NULL) { } // Write the new settings file. - if (file_put_contents(DRUPAL_ROOT . '/' . $settings_file, $buffer) === FALSE) { + if (file_put_contents($settings_file, $buffer) === FALSE) { throw new Exception(t('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file))); } else { diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 0f5b45aa88a..aaac1829b40 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -193,19 +193,23 @@ class FileStorage implements StorageInterface { * Implements Drupal\Core\Config\StorageInterface::listAll(). */ public function listAll($prefix = '') { - // glob() silently ignores the error of a non-existing search directory, - // even with the GLOB_ERR flag. $dir = $this->getCollectionDirectory(); - if (!file_exists($dir)) { + if (!is_dir($dir)) { return array(); } $extension = '.' . static::getFileExtension(); - // \GlobIterator on Windows requires an absolute path. - $files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension); + + // glob() directly calls into libc glob(), which is not aware of PHP stream + // wrappers. Same for \GlobIterator (which additionally requires an absolute + // realpath() on Windows). + // @see https://github.com/mikey179/vfsStream/issues/2 + $files = scandir($dir); $names = array(); foreach ($files as $file) { - $names[] = $file->getBasename($extension); + if ($file[0] !== '.' && fnmatch($prefix . '*' . $extension, $file)) { + $names[] = basename($file, $extension); + } } return $names; @@ -299,13 +303,15 @@ class FileStorage implements StorageInterface { $collections[] = $collection . '.' . $sub_collection; } } - // Check that the collection is valid by searching if for configuration + // Check that the collection is valid by searching it for configuration // objects. A directory without any configuration objects is not a valid // collection. - // \GlobIterator on Windows requires an absolute path. - $files = new \GlobIterator(realpath($directory . '/' . $collection) . '/*.' . $this->getFileExtension()); - if (count($files)) { - $collections[] = $collection; + // @see \Drupal\Core\Config\FileStorage::listAll() + foreach (scandir($directory . '/' . $collection) as $file) { + if ($file[0] !== '.' && fnmatch('*.' . $this->getFileExtension(), $file)) { + $collections[] = $collection; + break; + } } } } diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index 5c1d65a7794..084325020ab 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -195,10 +195,17 @@ class InstallStorage extends FileStorage { // We don't have to use ExtensionDiscovery here because our list of // extensions was already obtained through an ExtensionDiscovery scan. $directory = $this->getComponentFolder($extension_object); - if (file_exists($directory)) { - $files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension); + if (is_dir($directory)) { + // glob() directly calls into libc glob(), which is not aware of PHP + // stream wrappers. Same for \GlobIterator (which additionally requires + // an absolute realpath() on Windows). + // @see https://github.com/mikey179/vfsStream/issues/2 + $files = scandir($directory); + foreach ($files as $file) { - $folders[$file->getBasename($extension)] = $directory; + if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) { + $folders[basename($file, $extension)] = $directory; + } } } } @@ -215,10 +222,17 @@ class InstallStorage extends FileStorage { $extension = '.' . $this->getFileExtension(); $folders = array(); $directory = $this->getCoreFolder(); - if (file_exists($directory)) { - $files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension); + if (is_dir($directory)) { + // glob() directly calls into libc glob(), which is not aware of PHP + // stream wrappers. Same for \GlobIterator (which additionally requires an + // absolute realpath() on Windows). + // @see https://github.com/mikey179/vfsStream/issues/2 + $files = scandir($directory); + foreach ($files as $file) { - $folders[$file->getBasename($extension)] = $directory; + if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) { + $folders[basename($file, $extension)] = $directory; + } } } return $folders; diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 47474406433..cd0c19e8de9 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -15,6 +15,7 @@ use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\Config\NullStorage; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ServiceModifierInterface; use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Core\Extension\ExtensionDiscovery; @@ -151,13 +152,16 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { protected $serviceYamls; /** - * List of discovered service provider class names. + * List of discovered service provider class names or objects. * * This is a nested array whose top-level keys are 'app' and 'site', denoting * the origin of a service provider. Site-specific providers have to be * collected separately, because they need to be processed last, so as to be * able to override services from application service providers. * + * Allowing objects is for example used to allow + * \Drupal\KernelTests\KernelTestBase to register itself as service provider. + * * @var array */ protected $serviceProviderClasses; @@ -427,6 +431,21 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { return $this->container; } + /** + * {@inheritdoc} + */ + public function setContainer(ContainerInterface $container = NULL) { + if (isset($this->container)) { + throw new \Exception('The container should not override an existing container.'); + } + if ($this->booted) { + throw new \Exception('The container cannot be set after a booted kernel.'); + } + + $this->container = $container; + return $this; + } + /** * {@inheritdoc} */ @@ -514,7 +533,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { // Add site-specific service providers. if (!empty($GLOBALS['conf']['container_service_providers'])) { foreach ($GLOBALS['conf']['container_service_providers'] as $class) { - if (class_exists($class)) { + if ((is_string($class) && class_exists($class)) || (is_object($class) && ($class instanceof ServiceProviderInterface || $class instanceof ServiceModifierInterface))) { $this->serviceProviderClasses['site'][] = $class; } } @@ -745,6 +764,13 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { } } + // If we haven't booted yet but there is a container, then we're asked to + // boot the container injected via setContainer(). + // @see \Drupal\KernelTests\KernelTestBase::setUp() + if (isset($this->container) && !$this->booted) { + $container = $this->container; + } + // If the module list hasn't already been set in updateModules and we are // not forcing a rebuild, then try and load the container from the disk. if (empty($this->moduleList) && !$this->containerNeedsRebuild) { @@ -760,6 +786,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { } } + // If there is still no container, build a new one from scratch. if (!isset($container)) { $container = $this->compileContainer(); } @@ -1149,7 +1176,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { ); foreach ($this->serviceProviderClasses as $origin => $classes) { foreach ($classes as $name => $class) { - $this->serviceProviders[$origin][$name] = new $class; + if (!is_object($class)) { + $this->serviceProviders[$origin][$name] = new $class; + } + else { + $this->serviceProviders[$origin][$name] = $class; + } } } } diff --git a/core/lib/Drupal/Core/DrupalKernelInterface.php b/core/lib/Drupal/Core/DrupalKernelInterface.php index 892952a0698..12c32f63f80 100644 --- a/core/lib/Drupal/Core/DrupalKernelInterface.php +++ b/core/lib/Drupal/Core/DrupalKernelInterface.php @@ -7,6 +7,7 @@ namespace Drupal\Core; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -16,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request; * This interface extends Symfony's KernelInterface and adds methods for * responding to modules being enabled or disabled during its lifetime. */ -interface DrupalKernelInterface extends HttpKernelInterface { +interface DrupalKernelInterface extends HttpKernelInterface, ContainerAwareInterface { /** * Boots the current kernel. diff --git a/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php b/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php index 69598e1b129..5ca09712876 100644 --- a/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php +++ b/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php @@ -52,7 +52,7 @@ class PhpStorageFactory { $configuration['bin'] = $name; } if (!isset($configuration['directory'])) { - $configuration['directory'] = DRUPAL_ROOT . '/' . PublicStream::basePath() . '/php'; + $configuration['directory'] = PublicStream::basePath() . '/php'; } return new $class($configuration); } diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 5c609420042..021c541bb22 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -127,6 +127,15 @@ abstract class LocalStream implements StreamWrapperInterface { $uri = $this->uri; } $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri); + + // In PHPUnit tests, the base path for local streams may be a virtual + // filesystem stream wrapper URI, in which case this local stream acts like + // a proxy. realpath() is not supported by vfsStream, because a virtual + // file system does not have a real filepath. + if (strpos($path, 'vfs://') === 0) { + return $path; + } + $realpath = realpath($path); if (!$realpath) { // This file does not yet exist. diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php index d5bd0b9a721..9319cdde2c6 100644 --- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php @@ -331,10 +331,12 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { if (empty($langcodes)) { $langcodes = array(); // Collect languages included with CKEditor based on file listing. - $ckeditor_languages = new \GlobIterator(\Drupal::root() . '/core/assets/vendor/ckeditor/lang/*.js'); - foreach ($ckeditor_languages as $language_file) { - $langcode = $language_file->getBasename('.js'); - $langcodes[$langcode] = $langcode; + $files = scandir('core/assets/vendor/ckeditor/lang'); + foreach ($files as $file) { + if ($file[0] !== '.' && fnmatch('*.js', $file)) { + $langcode = basename($file, '.js'); + $langcodes[$langcode] = $langcode; + } } \Drupal::cache()->set('ckeditor.langcodes', $langcodes); } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index ba8622b0852..2c8c86235ff 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -33,6 +33,9 @@ use Symfony\Component\HttpFoundation\Request; * Additional modules needed in a test may be loaded and added to the fixed * module list. * + * @deprecated in Drupal 8.0.x, will be removed before Drupal 8.2.x. Use + * \Drupal\KernelTests\KernelTestBase instead. + * * @see \Drupal\simpletest\KernelTestBase::$modules * @see \Drupal\simpletest\KernelTestBase::enableModules() * diff --git a/core/modules/simpletest/src/RandomGeneratorTrait.php b/core/modules/simpletest/src/RandomGeneratorTrait.php new file mode 100644 index 00000000000..70087ce5db2 --- /dev/null +++ b/core/modules/simpletest/src/RandomGeneratorTrait.php @@ -0,0 +1,131 @@ +') character to ensure coverage for special + * characters and avoid the introduction of random test failures. + * + * @param int $length + * Length of random string to generate. + * + * @return string + * Pseudo-randomly generated unique string including special characters. + * + * @see \Drupal\Component\Utility\Random::string() + */ + public function randomString($length = 8) { + if ($length < 4) { + return $this->getRandomGenerator()->string($length, TRUE, array($this, 'randomStringValidate')); + } + + // To prevent the introduction of random test failures, ensure that the + // returned string contains a character that needs to be escaped in HTML by + // injecting an ampersand into it. + $replacement_pos = floor($length / 2); + // Remove 2 from the length to account for the ampersand and greater than + // characters. + $string = $this->getRandomGenerator()->string($length - 2, TRUE, array($this, 'randomStringValidate')); + return substr_replace($string, '>&', $replacement_pos, 0); + } + + /** + * Callback for random string validation. + * + * @see \Drupal\Component\Utility\Random::string() + * + * @param string $string + * The random string to validate. + * + * @return bool + * TRUE if the random string is valid, FALSE if not. + */ + public function randomStringValidate($string) { + // Consecutive spaces causes issues for + // \Drupal\simpletest\WebTestBase::assertLink(). + if (preg_match('/\s{2,}/', $string)) { + return FALSE; + } + + // Starting or ending with a space means that length might not be what is + // expected. + if (preg_match('/^\s|\s$/', $string)) { + return FALSE; + } + + return TRUE; + } + + /** + * Generates a unique random string containing letters and numbers. + * + * Do not use this method when testing unvalidated user input. Instead, use + * \Drupal\simpletest\TestBase::randomString(). + * + * @param int $length + * Length of random string to generate. + * + * @return string + * Randomly generated unique string. + * + * @see \Drupal\Component\Utility\Random::name() + */ + protected function randomMachineName($length = 8) { + return $this->getRandomGenerator()->name($length, TRUE); + } + + /** + * Generates a random PHP object. + * + * @param int $size + * The number of random keys to add to the object. + * + * @return \stdClass + * The generated object, with the specified number of random keys. Each key + * has a random string value. + * + * @see \Drupal\Component\Utility\Random::object() + */ + public function randomObject($size = 4) { + return $this->getRandomGenerator()->object($size); + } + + /** + * Gets the random generator for the utility methods. + * + * @return \Drupal\Component\Utility\Random + * The random generator. + */ + protected function getRandomGenerator() { + if (!is_object($this->randomGenerator)) { + $this->randomGenerator = new Random(); + } + return $this->randomGenerator; + } + +} diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index aa5ed496766..5e76459f368 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -28,6 +28,7 @@ use Drupal\Core\Utility\Error; abstract class TestBase { use SessionTestTrait; + use RandomGeneratorTrait; /** * The test run ID. @@ -283,13 +284,6 @@ abstract class TestBase { */ protected $configImporter; - /** - * The random generator. - * - * @var \Drupal\Component\Utility\Random - */ - protected $randomGenerator; - /** * Set to TRUE to strict check all configuration saved. * @@ -1417,113 +1411,6 @@ abstract class TestBase { new Settings($settings); } - /** - * Generates a pseudo-random string of ASCII characters of codes 32 to 126. - * - * Do not use this method when special characters are not possible (e.g., in - * machine or file names that have already been validated); instead, use - * \Drupal\simpletest\TestBase::randomMachineName(). If $length is greater - * than 3 the random string will include at least one ampersand ('&') and - * at least one greater than ('>') character to ensure coverage for special - * characters and avoid the introduction of random test failures. - * - * @param int $length - * Length of random string to generate. - * - * @return string - * Pseudo-randomly generated unique string including special characters. - * - * @see \Drupal\Component\Utility\Random::string() - */ - public function randomString($length = 8) { - if ($length < 4) { - return $this->getRandomGenerator()->string($length, TRUE, array($this, 'randomStringValidate')); - } - - // To prevent the introduction of random test failures, ensure that the - // returned string contains a character that needs to be escaped in HTML by - // injecting an ampersand into it. - $replacement_pos = floor($length / 2); - // Remove 2 from the length to account for the ampersand and greater than - // characters. - $string = $this->getRandomGenerator()->string($length - 2, TRUE, array($this, 'randomStringValidate')); - return substr_replace($string, '>&', $replacement_pos, 0); - } - - /** - * Callback for random string validation. - * - * @see \Drupal\Component\Utility\Random::string() - * - * @param string $string - * The random string to validate. - * - * @return bool - * TRUE if the random string is valid, FALSE if not. - */ - public function randomStringValidate($string) { - // Consecutive spaces causes issues for - // Drupal\simpletest\WebTestBase::assertLink(). - if (preg_match('/\s{2,}/', $string)) { - return FALSE; - } - - // Starting or ending with a space means that length might not be what is - // expected. - if (preg_match('/^\s|\s$/', $string)) { - return FALSE; - } - - return TRUE; - } - - /** - * Generates a unique random string containing letters and numbers. - * - * Do not use this method when testing unvalidated user input. Instead, use - * \Drupal\simpletest\TestBase::randomString(). - * - * @param int $length - * Length of random string to generate. - * - * @return string - * Randomly generated unique string. - * - * @see \Drupal\Component\Utility\Random::name() - */ - public function randomMachineName($length = 8) { - return $this->getRandomGenerator()->name($length, TRUE); - } - - /** - * Generates a random PHP object. - * - * @param int $size - * The number of random keys to add to the object. - * - * @return \stdClass - * The generated object, with the specified number of random keys. Each key - * has a random string value. - * - * @see \Drupal\Component\Utility\Random::object() - */ - public function randomObject($size = 4) { - return $this->getRandomGenerator()->object($size); - } - - /** - * Gets the random generator for the utility methods. - * - * @return \Drupal\Component\Utility\Random - * The random generator - */ - protected function getRandomGenerator() { - if (!is_object($this->randomGenerator)) { - $this->randomGenerator = new Random(); - } - return $this->randomGenerator; - } - /** * Converts a list of possible parameters into a stack of permutations. * diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php index f71027d4977..630cca774f1 100644 --- a/core/modules/simpletest/src/TestDiscovery.php +++ b/core/modules/simpletest/src/TestDiscovery.php @@ -81,6 +81,7 @@ class TestDiscovery { // Add PHPUnit test namespaces of Drupal core. $this->testNamespaces['Drupal\\Tests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/Tests']; + $this->testNamespaces['Drupal\\KernelTests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/KernelTests']; $this->testNamespaces['Drupal\\FunctionalTests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/FunctionalTests']; $this->availableExtensions = array(); @@ -98,6 +99,7 @@ class TestDiscovery { // Add PHPUnit test namespaces. $this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit"; + $this->testNamespaces["Drupal\\Tests\\$name\\Kernel\\"][] = "$base_path/tests/src/Kernel"; $this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional"; } diff --git a/core/modules/simpletest/tests/src/Unit/TestBaseTest.php b/core/modules/simpletest/tests/src/Unit/TestBaseTest.php index 1b2cb6b586b..bc2f2fe6376 100644 --- a/core/modules/simpletest/tests/src/Unit/TestBaseTest.php +++ b/core/modules/simpletest/tests/src/Unit/TestBaseTest.php @@ -117,7 +117,7 @@ class TestBaseTest extends UnitTestCase { $this->assertEquals($length, strlen($string)); // randomString() should always include an ampersand ('&') and a // greater than ('>') if $length is greater than 3. - if ($length > 3) { + if ($length > 4) { $this->assertContains('&', $string); $this->assertContains('>', $string); } diff --git a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php similarity index 91% rename from core/modules/system/src/Tests/Extension/ModuleHandlerTest.php rename to core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php index 7435949526e..3ca387210a7 100644 --- a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php +++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php @@ -5,11 +5,11 @@ * Contains \Drupal\system\Tests\Extension\ModuleHandlerTest. */ -namespace Drupal\system\Tests\Extension; +namespace Drupal\Tests\system\Kernel\Extension; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\simpletest\KernelTestBase; use \Drupal\Core\Extension\ModuleUninstallValidatorException; +use Drupal\KernelTests\KernelTestBase; /** * Tests ModuleHandler functionality. @@ -21,10 +21,13 @@ class ModuleHandlerTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = array('system'); - - public function setUp() { + protected function setUp() { parent::setUp(); + + // @todo ModuleInstaller calls system_rebuild_module_data which is part of + // system.module, see https://www.drupal.org/node/2208429. + include_once $this->root . '/core/modules/system/system.module'; + // Set up the state values so we know where to find the files when running // drupal_get_filename(). // @todo Remove as part of https://www.drupal.org/node/2186491 @@ -34,8 +37,8 @@ class ModuleHandlerTest extends KernelTestBase { /** * {@inheritdoc} */ - public function containerBuild(ContainerBuilder $container) { - parent::containerBuild($container); + public function register(ContainerBuilder $container) { + parent::register($container); // Put a fake route bumper on the container to be called during uninstall. $container ->register('router.dumper', 'Drupal\Core\Routing\NullMatcherDumper'); @@ -45,24 +48,9 @@ class ModuleHandlerTest extends KernelTestBase { * The basic functionality of retrieving enabled modules. */ function testModuleList() { - // Prime the drupal_get_filename() static cache with the location of the - // testing profile as it is not the currently active profile and we don't - // yet have any cached way to retrieve its location. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', 'testing', 'core/profiles/testing/testing.info.yml'); - // Build a list of modules, sorted alphabetically. - $profile_info = install_profile_info('testing', 'en'); - $module_list = $profile_info['dependencies']; + $module_list = array(); - // Installation profile is a module that is expected to be loaded. - $module_list[] = 'testing'; - - sort($module_list); - // Compare this list to the one returned by the module handler. We expect - // them to match, since all default profile modules have a weight equal to 0 - // (except for block.module, which has a lower weight but comes first in - // the alphabet anyway). - $this->assertModuleList($module_list, 'Testing profile'); + $this->assertModuleList($module_list, 'Initial'); // Try to install a new module. $this->moduleInstaller()->install(array('ban')); @@ -98,7 +86,6 @@ class ModuleHandlerTest extends KernelTestBase { protected function assertModuleList(Array $expected_values, $condition) { $expected_values = array_values(array_unique($expected_values)); $enabled_modules = array_keys($this->container->get('module_handler')->getModuleList()); - $enabled_modules = sort($enabled_modules); $this->assertEqual($expected_values, $enabled_modules, format_string('@condition: extension handler returns correct results', array('@condition' => $condition))); } @@ -196,7 +183,7 @@ class ModuleHandlerTest extends KernelTestBase { function testUninstallProfileDependency() { $profile = 'minimal'; $dependency = 'dblog'; - $this->settingsSet('install_profile', $profile); + $this->setSetting('install_profile', $profile); // Prime the drupal_get_filename() static cache with the location of the // minimal profile as it is not the currently active profile and we don't // yet have any cached way to retrieve its location. diff --git a/core/modules/system/src/Tests/PhpStorage/PhpStorageFactoryTest.php b/core/modules/system/tests/src/Kernel/PhpStorage/PhpStorageFactoryTest.php similarity index 86% rename from core/modules/system/src/Tests/PhpStorage/PhpStorageFactoryTest.php rename to core/modules/system/tests/src/Kernel/PhpStorage/PhpStorageFactoryTest.php index 580bfa10805..b05be968cee 100644 --- a/core/modules/system/src/Tests/PhpStorage/PhpStorageFactoryTest.php +++ b/core/modules/system/tests/src/Kernel/PhpStorage/PhpStorageFactoryTest.php @@ -5,14 +5,14 @@ * Contains \Drupal\system\Tests\PhpStorage\PhpStorageFactoryTest. */ -namespace Drupal\system\Tests\PhpStorage; +namespace Drupal\Tests\system\Kernel\PhpStorage; use Drupal\Component\PhpStorage\MTimeProtectedFileStorage; use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; -use Drupal\simpletest\KernelTestBase; use Drupal\system\PhpStorage\MockPhpStorage; +use Drupal\KernelTests\KernelTestBase; /** * Tests the PHP storage factory. @@ -22,6 +22,18 @@ use Drupal\system\PhpStorage\MockPhpStorage; */ class PhpStorageFactoryTest extends KernelTestBase { + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Empty the PHP storage settings, as KernelTestBase sets it by default. + $settings = Settings::getAll(); + unset($settings['php_storage']); + new Settings($settings); + } + /** * Tests the get() method with no settings. */ @@ -59,7 +71,7 @@ class PhpStorageFactoryTest extends KernelTestBase { $this->setSettings('test', array('directory' => NULL)); $php = PhpStorageFactory::get('test'); $this->assertTrue($php instanceof MockPhpStorage, 'An MockPhpStorage instance was returned from overridden settings.'); - $this->assertIdentical(\Drupal::root() . '/' . PublicStream::basePath() . '/php', $php->getConfigurationValue('directory'), 'Default file directory was used.'); + $this->assertIdentical(PublicStream::basePath() . '/php', $php->getConfigurationValue('directory'), 'Default file directory was used.'); // Test that a default storage class is set if it's empty. $this->setSettings('test', array('class' => NULL)); diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 743bf08159a..15b6316f79f 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -22,6 +22,16 @@ ./drush/tests + + ./tests/Drupal/KernelTests + ./modules/*/tests/src/Kernel + ../modules/*/tests/src/Kernel + ../sites/*/modules/*/tests/src/Kernel + + ./vendor + + ./drush/tests + ./tests/Drupal/FunctionalTests ./modules/*/tests/src/Functional diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 0ccdc350cd2..0755cf253c4 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -709,7 +709,7 @@ function simpletest_script_command($test_id, $test_class) { * @see simpletest_script_run_one_test() */ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { - if (strpos($test_class, 'Drupal\\Tests\\') === 0) { + if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) { // PHPUnit test, move on. return; } diff --git a/core/tests/Drupal/KernelTests/AssertLegacyTrait.php b/core/tests/Drupal/KernelTests/AssertLegacyTrait.php new file mode 100644 index 00000000000..6165871a5e0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/AssertLegacyTrait.php @@ -0,0 +1,129 @@ +assertEquals($expected, $actual, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::assertNotEqual() + * + * @deprecated Scheduled for removal in Drupal 9.0.0. Use + * self::assertNotEquals() instead. + */ + protected function assertNotEqual($actual, $expected, $message = '') { + $this->assertNotEquals($expected, $actual, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::assertIdentical() + * + * @deprecated Scheduled for removal in Drupal 9.0.0. Use self::assertSame() + * instead. + */ + protected function assertIdentical($actual, $expected, $message = '') { + $this->assertSame($expected, $actual, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::assertNotIdentical() + * + * @deprecated Scheduled for removal in Drupal 9.0.0. Use + * self::assertNotSame() instead. + */ + protected function assertNotIdentical($actual, $expected, $message = '') { + $this->assertNotSame($expected, $actual, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::assertIdenticalObject() + * + * @deprecated Scheduled for removal in Drupal 9.0.0. Use self::assertEquals() + * instead. + */ + protected function assertIdenticalObject($actual, $expected, $message = '') { + // Note: ::assertSame checks whether its the same object. ::assertEquals + // though compares + + $this->assertEquals($expected, $actual, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::pass() + * + * @deprecated Scheduled for removal in Drupal 9.0.0. Use self::assertTrue() + * instead. + */ + protected function pass($message) { + $this->assertTrue(TRUE, $message); + } + + /** + * @see \Drupal\simpletest\TestBase::verbose() + */ + protected function verbose($message) { + if (in_array('--debug', $_SERVER['argv'], TRUE)) { + // Write directly to STDOUT to not produce unexpected test output. + // The STDOUT stream does not obey output buffering. + fwrite(STDOUT, $message . "\n"); + } + } + +} diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php new file mode 100644 index 00000000000..3e769056010 --- /dev/null +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -0,0 +1,1051 @@ + ['parsedFiles'], + 'Drupal\Core\DependencyInjection\YamlFileLoader' => ['yaml'], + 'Drupal\Core\Extension\ExtensionDiscovery' => ['files'], + 'Drupal\Core\Extension\InfoParser' => ['parsedInfos'], + // Drupal::$container cannot be serialized. + 'Drupal' => ['container'], + // Settings cannot be serialized. + 'Drupal\Core\Site\Settings' => ['instance'], + ]; + + /** + * {@inheritdoc} + * + * Do not forward any global state from the parent process to the processes + * that run the actual tests. + * + * @see self::runTestInSeparateProcess + */ + protected $preserveGlobalState = FALSE; + + /** + * @var \Composer\Autoload\Classloader + */ + protected $classLoader; + + /** + * @var string + */ + protected $siteDirectory; + + /** + * @var string + */ + protected $databasePrefix; + + /** + * @var \Drupal\Core\DependencyInjection\ContainerBuilder + */ + protected $container; + + /** + * @var \Drupal\Core\DependencyInjection\ContainerBuilder + */ + private static $initialContainerBuilder; + + /** + * Modules to enable. + * + * Test classes extending this class, and any classes in the hierarchy up to + * this class, may specify individual lists of modules to enable by setting + * this property. The values of all properties in all classes in the class + * hierarchy are merged. + * + * @see \Drupal\Tests\KernelTestBase::enableModules() + * @see \Drupal\Tests\KernelTestBase::bootKernel() + * + * @var array + */ + public static $modules = array(); + + /** + * The virtual filesystem root directory. + * + * @var \org\bovigo\vfs\vfsStreamDirectory + */ + protected $vfsRoot; + + /** + * @var int + */ + protected $expectedLogSeverity; + + /** + * @var string + */ + protected $expectedLogMessage; + + /** + * @todo Move into Config test base class. + * @var \Drupal\Core\Config\ConfigImporter + */ + protected $configImporter; + + /** + * The app root. + * + * @var string + */ + protected $root; + + /** + * Set to TRUE to strict check all configuration saved. + * + * @see \Drupal\Core\Config\Testing\ConfigSchemaChecker + * + * @var bool + */ + protected $strictConfigSchema = TRUE; + + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass() { + parent::setUpBeforeClass(); + + // Change the current dir to DRUPAL_ROOT. + chdir(static::getDrupalRoot()); + } + + /** + * Returns the drupal root directory. + * + * @return string + */ + protected static function getDrupalRoot() { + return dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->root = static::getDrupalRoot(); + $this->bootEnvironment(); + $this->bootKernel(); + } + + /** + * Bootstraps a basic test environment. + * + * Should not be called by tests. Only visible for DrupalKernel integration + * tests. + * + * @see \Drupal\system\Tests\DrupalKernel\DrupalKernelTest + * @internal + */ + protected function bootEnvironment() { + $this->streamWrappers = array(); + \Drupal::unsetContainer(); + + // @see /core/tests/bootstrap.php + $this->classLoader = $GLOBALS['loader']; + + require_once $this->root . '/core/includes/bootstrap.inc'; + + // Set up virtual filesystem. + // Ensure that the generated test site directory does not exist already, + // which may happen with a large amount of concurrent threads and + // long-running tests. + do { + $suffix = mt_rand(100000, 999999); + $this->siteDirectory = 'sites/simpletest/' . $suffix; + $this->databasePrefix = 'simpletest' . $suffix; + } while (is_dir($this->root . '/' . $this->siteDirectory)); + + $this->vfsRoot = vfsStream::setup('root', NULL, array( + 'sites' => array( + 'simpletest' => array( + $suffix => array(), + ), + ), + )); + $this->siteDirectory = vfsStream::url('root/sites/simpletest/' . $suffix); + + mkdir($this->siteDirectory . '/files', 0775); + mkdir($this->siteDirectory . '/files/config/' . CONFIG_ACTIVE_DIRECTORY, 0775, TRUE); + mkdir($this->siteDirectory . '/files/config/' . CONFIG_STAGING_DIRECTORY, 0775, TRUE); + + // Ensure that all code that relies on drupal_valid_test_ua() can still be + // safely executed. This primarily affects the (test) site directory + // resolution (used by e.g. LocalStream and PhpStorage). + $this->databasePrefix = 'simpletest' . $suffix; + drupal_valid_test_ua($this->databasePrefix); + + $settings = array( + 'hash_salt' => get_class($this), + 'file_public_path' => $this->siteDirectory . '/files', + // Disable Twig template caching/dumping. + 'twig_cache' => FALSE, + // @see \Drupal\KernelTests\KernelTestBase::register() + ); + new Settings($settings); + + $GLOBALS['config_directories'] = array( + CONFIG_ACTIVE_DIRECTORY => $this->siteDirectory . '/files/config/active', + CONFIG_STAGING_DIRECTORY => $this->siteDirectory . '/files/config/staging', + ); + + foreach (Database::getAllConnectionInfo() as $key => $targets) { + Database::removeConnection($key); + } + Database::addConnectionInfo('default', 'default', $this->getDatabaseConnectionInfo()['default']); + } + + /** + * Bootstraps a kernel for a test. + */ + private function bootKernel() { + $this->setSetting('container_yamls', []); + // Allow for test-specific overrides. + $settings_services_file = $this->root . '/sites/default' . '/testing.services.yml'; + if (file_exists($settings_services_file)) { + // Copy the testing-specific service overrides in place. + $testing_services_file = $this->root . '/' . $this->siteDirectory . '/services.yml'; + copy($settings_services_file, $testing_services_file); + $this->setSetting('container_yamls', [$testing_services_file]); + } + + // Allow for global test environment overrides. + if (file_exists($test_env = $this->root . '/sites/default/testing.services.yml')) { + $GLOBALS['conf']['container_yamls']['testing'] = $test_env; + } + // Add this test class as a service provider. + $GLOBALS['conf']['container_service_providers']['test'] = $this; + + $modules = self::getModulesToEnable(get_class($this)); + + // Prepare a precompiled container for all tests of this class. + // Substantially improves performance, since ContainerBuilder::compile() + // is very expensive. Encourages testing best practices (small tests). + // Normally a setUpBeforeClass() operation, but object scope is required to + // inject $this test class instance as a service provider (see above). + $rc = new \ReflectionClass(get_class($this)); + $test_method_count = count(array_filter($rc->getMethods(), function ($method) { + // PHPUnit's @test annotations are intentionally ignored/not supported. + return strpos($method->getName(), 'test') === 0; + })); + if ($test_method_count > 1 && !$this->isTestInIsolation()) { + // Clone a precompiled, empty ContainerBuilder instance for each test. + $container = $this->getCompiledContainerBuilder($modules); + } + + // Bootstrap the kernel. Do not use createFromRequest() to retain Settings. + $kernel = new DrupalKernel('testing', $this->classLoader, FALSE); + $kernel->setSitePath($this->siteDirectory); + // Boot the precompiled container. The kernel will enhance it with synthetic + // services. + if (isset($container)) { + $kernel->setContainer($container); + unset($container); + } + // Boot a new one-time container from scratch. Ensure to set the module list + // upfront to avoid a subsequent rebuild. + elseif ($modules && $extensions = $this->getExtensionsForModules($modules)) { + $kernel->updateModules($extensions, $extensions); + } + // DrupalKernel::boot() is not sufficient as it does not invoke preHandle(), + // which is required to initialize legacy global variables. + $request = Request::create('/'); + $kernel->prepareLegacyRequest($request); + + // register() is only called if a new container was built/compiled. + $this->container = $kernel->getContainer(); + + if ($modules) { + $this->container->get('module_handler')->loadAll(); + } + + // Write the core.extension configuration. + // Required for ConfigInstaller::installDefaultConfig() to work. + $this->container->get('config.storage')->write('core.extension', array( + 'module' => array_fill_keys($modules, 0), + 'theme' => array(), + )); + + $settings = Settings::getAll(); + $settings['php_storage']['default'] = [ + 'class' => '\Drupal\Component\PhpStorage\FileStorage', + ]; + new Settings($settings); + } + + /** + * Configuration accessor for tests. Returns non-overridden configuration. + * + * @param string $name + * The configuration name. + * + * @return \Drupal\Core\Config\Config + * The configuration object with original configuration data. + */ + protected function config($name) { + return $this->container->get('config.factory')->getEditable($name); + } + + /** + * Returns the Database connection info to be used for this test. + * + * This method only exists for tests of the Database component itself, because + * they require multiple database connections. Each SQLite :memory: connection + * creates a new/separate database in memory. A shared-memory SQLite file URI + * triggers PHP open_basedir/allow_url_fopen/allow_url_include restrictions. + * Due to that, Database tests are running against a SQLite database that is + * located in an actual file in the system's temporary directory. + * + * Other tests should not override this method. + * + * @return array + * A Database connection info array. + * + * @internal + */ + protected function getDatabaseConnectionInfo() { + // If the test is run with argument dburl then use it. + $db_url = getenv('SIMPLETEST_DB'); + if (!empty($db_url)) { + $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root); + Database::addConnectionInfo('default', 'default', $database); + } + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + if (is_null($connection_info)) { + throw new \InvalidArgumentException('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable, like "sqlite://localhost//tmp/test.sqlite", to run PHPUnit based functional tests outside of run-tests.sh.'); + } + else { + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + // Replace the full table prefix definition to ensure that no table + // prefixes of the test runner leak into the test. + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + } + return $connection_info; + } + + /** + * Prepares a precompiled ContainerBuilder for all tests of this class. + * + * Avoids repetitive calls to ContainerBuilder::compile(), which is very slow. + * + * Based on the (always identical) list of $modules to enable, an initial + * container is compiled, all instantiated services are reset/removed, and + * this precompiled container is stored in a static class property. (Static, + * because PHPUnit instantiates a new class instance for each test *method*.) + * + * This method is not invoked if there is only a single test method. It is + * also not invoked for tests running in process isolation (since each test + * method runs in a separate process). + * + * The ContainerBuilder is not dumped into the filesystem (which would yield + * an actually compiled Container class), because + * + * 1. PHP code cannot be unloaded, so e.g. 900 tests would load 900 different, + * full Container classes into memory, quickly exceeding any sensible + * memory consumption (GigaBytes). + * 2. Dumping a Container class requires to actually write to the system's + * temporary directory. This is not really easy with vfs, because vfs + * doesn't support yet "include 'vfs://container.php'.". Maybe we could fix + * that upstream. + * 3. PhpDumper is very slow on its own. + * + * @param string[] $modules + * The list of modules to enable. + * + * @return \Drupal\Core\DependencyInjection\ContainerBuilder + * A clone of the precompiled, empty service container. + */ + private function getCompiledContainerBuilder(array $modules) { + if (!isset(self::$initialContainerBuilder)) { + $kernel = new DrupalKernel('testing', $this->classLoader, FALSE); + $kernel->setSitePath($this->siteDirectory); + if ($modules && $extensions = $this->getExtensionsForModules($modules)) { + $kernel->updateModules($extensions, $extensions); + } + $kernel->boot(); + self::$initialContainerBuilder = $kernel->getContainer(); + + // Remove all instantiated services, so the container is safe for cloning. + // Technically, ContainerBuilder::set($id, NULL) removes each definition, + // but the container is compiled/frozen already. + foreach (self::$initialContainerBuilder->getServiceIds() as $id) { + self::$initialContainerBuilder->set($id, NULL); + } + + // Destruct and trigger garbage collection. + \Drupal::unsetContainer(); + $kernel->shutdown(); + $kernel = NULL; + // @see register() + $this->container = NULL; + } + + $container = clone self::$initialContainerBuilder; + + return $container; + } + + /** + * Returns Extension objects for $modules to enable. + * + * @param string[] $modules + * The list of modules to enable. + * + * @return \Drupal\Core\Extension\Extension[] + * Extension objects for $modules, keyed by module name. + * + * @throws \PHPUnit_Framework_Exception + * If a module is not available. + * + * @see \Drupal\Tests\KernelTestBase::enableModules() + * @see \Drupal\Core\Extension\ModuleHandler::add() + */ + private function getExtensionsForModules(array $modules) { + $extensions = array(); + $discovery = new ExtensionDiscovery($this->root); + $discovery->setProfileDirectories(array()); + $list = $discovery->scan('module'); + foreach ($modules as $name) { + if (!isset($list[$name])) { + throw new \PHPUnit_Framework_Exception("Unavailable module: '$name'. If this module needs to be downloaded separately, annotate the test class with '@requires module $name'."); + } + $extensions[$name] = $list[$name]; + } + return $extensions; + } + + /** + * Registers test-specific services. + * + * Extend this method in your test to register additional services. This + * method is called whenever the kernel is rebuilt. + * + * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container + * The service container to enhance. + * + * @see \Drupal\Tests\KernelTestBase::bootKernel() + */ + public function register(ContainerBuilder $container) { + // Keep the container object around for tests. + $this->container = $container; + + $container + ->register('flood', 'Drupal\Core\Flood\MemoryBackend') + ->addArgument(new Reference('request_stack')); + $container + ->register('lock', 'Drupal\Core\Lock\NullLockBackend'); + $container + ->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); + $container + ->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory') + // Must persist container rebuilds, or all data would vanish otherwise. + ->addTag('persist'); + $container + ->setAlias('keyvalue', 'keyvalue.memory'); + + if ($this->strictConfigSchema) { + $container + ->register('simpletest.config_schema_checker', 'Drupal\Core\Config\Testing\ConfigSchemaChecker') + ->addArgument(new Reference('config.typed')) + ->addTag('event_subscriber'); + } + + if ($container->hasDefinition('path_processor_alias')) { + // Prevent the alias-based path processor, which requires a url_alias db + // table, from being registered to the path processor manager. We do this + // by removing the tags that the compiler pass looks for. This means the + // url generator can safely be used within tests. + $container->getDefinition('path_processor_alias') + ->clearTag('path_processor_inbound') + ->clearTag('path_processor_outbound'); + } + + if ($container->hasDefinition('password')) { + $container->getDefinition('password') + ->setArguments(array(1)); + } + } + + /** + * {@inheritdoc} + */ + protected function assertPostConditions() { + // Execute registered Drupal shutdown functions prior to tearing down. + // @see _drupal_shutdown_function() + $callbacks = &drupal_register_shutdown_function(); + while ($callback = array_shift($callbacks)) { + call_user_func_array($callback['callback'], $callback['arguments']); + } + + // Shut down the kernel (if bootKernel() was called). + // @see \Drupal\system\Tests\DrupalKernel\DrupalKernelTest + if ($this->container) { + $this->container->get('kernel')->shutdown(); + } + + // Fail in case any (new) shutdown functions exist. + $this->assertCount(0, drupal_register_shutdown_function(), 'Unexpected Drupal shutdown callbacks exist after running shutdown functions.'); + + parent::assertPostConditions(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // Destroy the testing kernel. + if (isset($this->kernel)) { + $this->kernel->shutdown(); + } + + // Free up memory: Own properties. + $this->classLoader = NULL; + $this->vfsRoot = NULL; + $this->configImporter = NULL; + + // Free up memory: Custom test class properties. + // Note: Private properties cannot be cleaned up. + $rc = new \ReflectionClass(__CLASS__); + $blacklist = array(); + foreach ($rc->getProperties() as $property) { + $blacklist[$property->name] = $property->getDeclaringClass()->name; + } + $rc = new \ReflectionClass($this); + foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isStatic() && !isset($blacklist[$property->name])) { + $this->{$property->name} = NULL; + } + } + + // Clean up statics, container, and settings. + if (function_exists('drupal_static_reset')) { + drupal_static_reset(); + } + \Drupal::unsetContainer(); + $this->container = NULL; + new Settings(array()); + + // Destroy the database connection, which for example removes the memory + // from sqlite in memory. + foreach (Database::getAllConnectionInfo() as $key => $targets) { + Database::removeConnection($key); + } + + parent::tearDown(); + } + + /** + * {@inheritdoc} + */ + public static function tearDownAfterClass() { + // Free up memory: Precompiled container. + self::$initialContainerBuilder = NULL; + parent::tearDownAfterClass(); + } + + /** + * Installs default configuration for a given list of modules. + * + * @param string|string[] $modules + * A list of modules for which to install default configuration. + * + * @throws \LogicException + * If any module in $modules is not enabled. + */ + protected function installConfig($modules) { + foreach ((array) $modules as $module) { + if (!$this->container->get('module_handler')->moduleExists($module)) { + throw new \LogicException("$module module is not enabled."); + } + $this->container->get('config.installer')->installDefaultConfig('module', $module); + } + } + + /** + * Installs database tables from a module schema definition. + * + * @param string $module + * The name of the module that defines the table's schema. + * @param string|array $tables + * The name or an array of the names of the tables to install. + * + * @throws \LogicException + * If $module is not enabled or the table schema cannot be found. + */ + protected function installSchema($module, $tables) { + // drupal_get_module_schema() is technically able to install a schema + // of a non-enabled module, but its ability to load the module's .install + // file depends on many other factors. To prevent differences in test + // behavior and non-reproducible test failures, we only allow the schema of + // explicitly loaded/enabled modules to be installed. + if (!$this->container->get('module_handler')->moduleExists($module)) { + throw new \LogicException("$module module is not enabled."); + } + $tables = (array) $tables; + foreach ($tables as $table) { + $schema = drupal_get_module_schema($module, $table); + if (empty($schema)) { + throw new \LogicException("$module module does not define a schema for table '$table'."); + } + $this->container->get('database')->schema()->createTable($table, $schema); + } + } + + /** + * Installs the storage schema for a specific entity type. + * + * @param string $entity_type_id + * The ID of the entity type. + */ + protected function installEntitySchema($entity_type_id) { + /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */ + $entity_manager = $this->container->get('entity.manager'); + $entity_type = $entity_manager->getDefinition($entity_type_id); + $entity_manager->onEntityTypeCreate($entity_type); + + // For test runs, the most common storage backend is a SQL database. For + // this case, ensure the tables got created. + $storage = $entity_manager->getStorage($entity_type_id); + if ($storage instanceof SqlEntityStorageInterface) { + $tables = $storage->getTableMapping()->getTableNames(); + $db_schema = $this->container->get('database')->schema(); + $all_tables_exist = TRUE; + foreach ($tables as $table) { + if (!$db_schema->tableExists($table)) { + $this->fail(SafeMarkup::format('Installed entity type table for the %entity_type entity type: %table', array( + '%entity_type' => $entity_type_id, + '%table' => $table, + ))); + $all_tables_exist = FALSE; + } + } + if ($all_tables_exist) { + $this->pass(SafeMarkup::format('Installed entity type tables for the %entity_type entity type: %tables', array( + '%entity_type' => $entity_type_id, + '%tables' => '{' . implode('}, {', $tables) . '}', + ))); + } + } + } + + /** + * Enables modules for this test. + * + * @param string[] $modules + * A list of modules to enable. Dependencies are not resolved; i.e., + * multiple modules have to be specified individually. The modules are only + * added to the active module list and loaded; i.e., their database schema + * is not installed. hook_install() is not invoked. A custom module weight + * is not applied. + * + * @throws \LogicException + * If any module in $modules is already enabled. + * @throws \RuntimeException + * If a module is not enabled after enabling it. + */ + protected function enableModules(array $modules) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + if ($trace[1]['function'] === 'setUp') { + trigger_error('KernelTestBase::enableModules() should not be called from setUp(). Use the $modules property instead.', E_DEPRECATED); + } + unset($trace); + + // Perform an ExtensionDiscovery scan as this function may receive a + // profile that is not the current profile, and we don't yet have a cached + // way to receive inactive profile information. + // @todo Remove as part of https://www.drupal.org/node/2186491 + $listing = new ExtensionDiscovery(\Drupal::root()); + $module_list = $listing->scan('module'); + // In ModuleHandlerTest we pass in a profile as if it were a module. + $module_list += $listing->scan('profile'); + + // Set the list of modules in the extension handler. + $module_handler = $this->container->get('module_handler'); + + // Write directly to active storage to avoid early instantiation of + // the event dispatcher which can prevent modules from registering events. + $active_storage = $this->container->get('config.storage'); + $extension_config = $active_storage->read('core.extension'); + + foreach ($modules as $module) { + if ($module_handler->moduleExists($module)) { + throw new \LogicException("$module module is already enabled."); + } + $module_handler->addModule($module, $module_list[$module]->getPath()); + // Maintain the list of enabled modules in configuration. + $extension_config['module'][$module] = 0; + } + $active_storage->write('core.extension', $extension_config); + + // Update the kernel to make their services available. + $extensions = $module_handler->getModuleList(); + $this->container->get('kernel')->updateModules($extensions, $extensions); + + // Ensure isLoaded() is TRUE in order to make + // \Drupal\Core\Theme\ThemeManagerInterface::render() work. + // Note that the kernel has rebuilt the container; this $module_handler is + // no longer the $module_handler instance from above. + $module_handler = $this->container->get('module_handler'); + $module_handler->reload(); + foreach ($modules as $module) { + if (!$module_handler->moduleExists($module)) { + throw new \RuntimeException("$module module is not enabled after enabling it."); + } + } + } + + /** + * Disables modules for this test. + * + * @param string[] $modules + * A list of modules to disable. Dependencies are not resolved; i.e., + * multiple modules have to be specified with dependent modules first. + * Code of previously enabled modules is still loaded. The modules are only + * removed from the active module list. + * + * @throws \LogicException + * If any module in $modules is already disabled. + * @throws \RuntimeException + * If a module is not disabled after disabling it. + */ + protected function disableModules(array $modules) { + // Unset the list of modules in the extension handler. + $module_handler = $this->container->get('module_handler'); + $module_filenames = $module_handler->getModuleList(); + $extension_config = $this->config('core.extension'); + foreach ($modules as $module) { + if (!$module_handler->moduleExists($module)) { + throw new \LogicException("$module module cannot be disabled because it is not enabled."); + } + unset($module_filenames[$module]); + $extension_config->clear('module.' . $module); + } + $extension_config->save(); + $module_handler->setModuleList($module_filenames); + $module_handler->resetImplementations(); + // Update the kernel to remove their services. + $this->container->get('kernel')->updateModules($module_filenames, $module_filenames); + + // Ensure isLoaded() is TRUE in order to make _theme() work. + // Note that the kernel has rebuilt the container; this $module_handler is + // no longer the $module_handler instance from above. + $module_handler = $this->container->get('module_handler'); + $module_handler->reload(); + foreach ($modules as $module) { + if ($module_handler->moduleExists($module)) { + throw new \RuntimeException("$module module is not disabled after disabling it."); + } + } + } + + /** + * Renders a render array. + * + * @param array $elements + * The elements to render. + * + * @return string + * The rendered string output (typically HTML). + */ + protected function render(array &$elements) { + $content = $this->container->get('renderer')->render($elements); + drupal_process_attached($elements); + $this->setRawContent($content); + $this->verbose('
' . SafeMarkup::checkPlain($content));
+    return $content;
+  }
+
+  /**
+   * Sets an in-memory Settings variable.
+   *
+   * @param string $name
+   *   The name of the setting to set.
+   * @param bool|string|int|array|null $value
+   *   The value to set. Note that array values are replaced entirely; use
+   *   \Drupal\Core\Site\Settings::get() to perform custom merges.
+   */
+  protected function setSetting($name, $value) {
+    $settings = Settings::getAll();
+    $settings[$name] = $value;
+    new Settings($settings);
+  }
+
+  /**
+   * Returns a ConfigImporter object to import test configuration.
+   *
+   * @return \Drupal\Core\Config\ConfigImporter
+   *
+   * @todo Move into Config-specific test base class.
+   */
+  protected function configImporter() {
+    if (!$this->configImporter) {
+      // Set up the ConfigImporter object for testing.
+      $storage_comparer = new StorageComparer(
+        $this->container->get('config.storage.staging'),
+        $this->container->get('config.storage'),
+        $this->container->get('config.manager')
+      );
+      $this->configImporter = new ConfigImporter(
+        $storage_comparer,
+        $this->container->get('event_dispatcher'),
+        $this->container->get('config.manager'),
+        $this->container->get('lock'),
+        $this->container->get('config.typed'),
+        $this->container->get('module_handler'),
+        $this->container->get('module_installer'),
+        $this->container->get('theme_handler'),
+        $this->container->get('string_translation')
+      );
+    }
+    // Always recalculate the changelist when called.
+    return $this->configImporter->reset();
+  }
+
+  /**
+   * Copies configuration objects from a source storage to a target storage.
+   *
+   * @param \Drupal\Core\Config\StorageInterface $source_storage
+   *   The source config storage.
+   * @param \Drupal\Core\Config\StorageInterface $target_storage
+   *   The target config storage.
+   *
+   * @todo Move into Config-specific test base class.
+   */
+  protected function copyConfig(StorageInterface $source_storage, StorageInterface $target_storage) {
+    $target_storage->deleteAll();
+    foreach ($source_storage->listAll() as $name) {
+      $target_storage->write($name, $source_storage->read($name));
+    }
+  }
+
+  /**
+   * Stops test execution.
+   */
+  protected function stop() {
+    $this->getTestResultObject()->stop();
+  }
+
+  /**
+   * Dumps the current state of the virtual filesystem to STDOUT.
+   */
+  protected function vfsDump() {
+    vfsStream::inspect(new vfsStreamPrintVisitor());
+  }
+
+  /**
+   * Returns the modules to enable for this test.
+   *
+   * @param string $class
+   *   The fully-qualified class name of this test.
+   *
+   * @return array
+   */
+  private static function getModulesToEnable($class) {
+    $modules = array();
+    while ($class) {
+      if (property_exists($class, 'modules')) {
+        // Only add the modules, if the $modules property was not inherited.
+        $rp = new \ReflectionProperty($class, 'modules');
+        if ($rp->class == $class) {
+          $modules[$class] = $class::$modules;
+        }
+      }
+      $class = get_parent_class($class);
+    }
+    // Modules have been collected in reverse class hierarchy order; modules
+    // defined by base classes should be sorted first. Then, merge the results
+    // together.
+    $modules = array_reverse($modules);
+    return call_user_func_array('array_merge_recursive', $modules);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareTemplate(\Text_Template $template) {
+    $bootstrap_globals = '';
+
+    // Fix missing bootstrap.php when $preserveGlobalState is FALSE.
+    // @see https://github.com/sebastianbergmann/phpunit/pull/797
+    $bootstrap_globals .= '$__PHPUNIT_BOOTSTRAP = ' . var_export($GLOBALS['__PHPUNIT_BOOTSTRAP'], TRUE) . ";\n";
+
+    // Avoid repetitive test namespace discoveries to improve performance.
+    // @see /core/tests/bootstrap.php
+    $bootstrap_globals .= '$namespaces = ' . var_export($GLOBALS['namespaces'], TRUE) . ";\n";
+
+    $template->setVar(array(
+      'constants' => '',
+      'included_files' => '',
+      'globals' => $bootstrap_globals,
+    ));
+  }
+
+  /**
+   * Returns whether the current test runs in isolation.
+   *
+   * @return bool
+   *
+   * @see https://github.com/sebastianbergmann/phpunit/pull/1350
+   */
+  protected function isTestInIsolation() {
+    return function_exists('__phpunit_run_isolated_test');
+  }
+
+  /**
+   * BC: Automatically resolve former KernelTestBase class properties.
+   *
+   * Test authors should follow the provided instructions and adjust their tests
+   * accordingly.
+   *
+   * @deprecated in Drupal 8.0.x, will be removed before Drupal 8.2.0.
+   */
+  public function __get($name) {
+    if (in_array($name, array(
+      'public_files_directory',
+      'private_files_directory',
+      'temp_files_directory',
+      'translation_files_directory',
+    ))) {
+      // @comment it in again.
+      trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use the regular API method to retrieve it instead (e.g., Settings).", $name), E_USER_DEPRECATED);
+      switch ($name) {
+        case 'public_files_directory':
+          return Settings::get('file_public_path', conf_path() . '/files');
+
+        case 'private_files_directory':
+          return $this->container->get('config.factory')->get('system.file')->get('path.private');
+
+        case 'temp_files_directory':
+          return file_directory_temp();
+
+        case 'translation_files_directory':
+          return Settings::get('file_public_path', conf_path() . '/translations');
+      }
+    }
+
+    if ($name === 'configDirectories') {
+      trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use config_get_config_directory() directly instead.", $name), E_DEPRECATED);
+      return array(
+        CONFIG_ACTIVE_DIRECTORY => config_get_config_directory(CONFIG_ACTIVE_DIRECTORY),
+        CONFIG_STAGING_DIRECTORY => config_get_config_directory(CONFIG_STAGING_DIRECTORY),
+      );
+    }
+
+    $denied = array(
+      // @see \Drupal\simpletest\TestBase
+      'testId',
+      'timeLimit',
+      'results',
+      'assertions',
+      'skipClasses',
+      'verbose',
+      'verboseId',
+      'verboseClassName',
+      'verboseDirectory',
+      'verboseDirectoryUrl',
+      'dieOnFail',
+      'kernel',
+      // @see \Drupal\simpletest\TestBase::prepareEnvironment()
+      'generatedTestFiles',
+      // @see \Drupal\simpletest\KernelTestBase::containerBuild()
+      'keyValueFactory',
+    );
+    if (in_array($name, $denied) || strpos($name, 'original') === 0) {
+      throw new \RuntimeException(sprintf('TestBase::$%s property no longer exists', $name));
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
new file mode 100644
index 00000000000..2deaa958b4d
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
@@ -0,0 +1,219 @@
+assertSame(realpath(__DIR__ . '/../../../../'), getcwd());
+  }
+
+  /**
+   * @covers ::bootEnvironment
+   */
+  public function testBootEnvironment() {
+    $this->assertRegExp('/^simpletest\d{6}$/', $this->databasePrefix);
+    $this->assertStringStartsWith('vfs://root/sites/simpletest/', $this->siteDirectory);
+    $this->assertEquals(array(
+      'root' => array(
+        'sites' => array(
+          'simpletest' => array(
+            substr($this->databasePrefix, 10) => array(
+              'files' => array(
+                'config' => array(
+                  'active' => array(),
+                  'staging' => array(),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    ), vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());
+  }
+
+  /**
+   * @covers ::getDatabaseConnectionInfo
+   */
+  public function testGetDatabaseConnectionInfoWithOutManualSetDbUrl() {
+    $this->setUp();
+
+    $options = $this->container->get('database')->getConnectionOptions();
+    $this->assertSame($this->databasePrefix, $options['prefix']['default']);
+  }
+
+  /**
+   * @covers ::getDatabaseConnectionInfo
+   */
+  public function testGetDatabaseConnectionInfoWithManualSetDbUrl() {
+    if (!file_exists('/tmp')) {
+      $this->markTestSkipped();
+    }
+    putenv('SIMPLETEST_DB=sqlite://localhost//tmp/test2.sqlite');
+    $this->setUp();
+
+    $options = $this->container->get('database')->getConnectionOptions();
+    $this->assertNotEqual('', $options['prefix']['default']);
+  }
+
+  /**
+   * @covers ::setUp
+   */
+  public function testSetUp() {
+    $this->assertTrue($this->container->has('request_stack'));
+    $this->assertTrue($this->container->initialized('request_stack'));
+    $request = $this->container->get('request_stack')->getCurrentRequest();
+    $this->assertNotEmpty($request);
+    $this->assertEquals('/', $request->getPathInfo());
+
+    $this->assertSame($request, \Drupal::request());
+
+    $this->assertEquals($this, $GLOBALS['conf']['container_service_providers']['test']);
+
+    $GLOBALS['destroy-me'] = TRUE;
+    $this->assertArrayHasKey('destroy-me', $GLOBALS);
+
+    $schema = $this->container->get('database')->schema();
+    $schema->createTable('foo', array(
+      'fields' => array(
+        'number' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+        ),
+      ),
+    ));
+    $this->assertTrue($schema->tableExists('foo'));
+  }
+
+  /**
+   * @covers ::setUp
+   * @depends testSetUp
+   */
+  public function testSetUpDoesNotLeak() {
+    $this->assertArrayNotHasKey('destroy-me', $GLOBALS);
+
+    // Ensure that we have a different database prefix.
+    $schema = $this->container->get('database')->schema();
+    $this->assertFalse($schema->tableExists('foo'));
+  }
+
+  /**
+   * @covers ::register
+   */
+  public function testRegister() {
+    // Verify that this container is identical to the actual container.
+    $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $this->container);
+    $this->assertSame($this->container, \Drupal::getContainer());
+
+    // The request service should never exist.
+    $this->assertFalse($this->container->has('request'));
+
+    // Verify that there is a request stack.
+    $request = $this->container->get('request_stack')->getCurrentRequest();
+    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Request', $request);
+    $this->assertSame($request, \Drupal::request());
+
+    // Trigger a container rebuild.
+    $this->enableModules(array('system'));
+
+    // Verify that this container is identical to the actual container.
+    $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $this->container);
+    $this->assertSame($this->container, \Drupal::getContainer());
+
+    // The request service should never exist.
+    $this->assertFalse($this->container->has('request'));
+
+    // Verify that there is a request stack (and that it persisted).
+    $new_request = $this->container->get('request_stack')->getCurrentRequest();
+    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Request', $new_request);
+    $this->assertSame($new_request, \Drupal::request());
+    $this->assertSame($request, $new_request);
+  }
+
+  /**
+   * @covers ::getCompiledContainerBuilder
+   *
+   * The point of this test is to have integration level testing.
+   */
+  public function testCompiledContainer() {
+    $this->enableModules(['system', 'user']);
+    $this->assertNull($this->installConfig('user'));
+  }
+
+  /**
+   * @covers ::getCompiledContainerBuilder
+   * @depends testCompiledContainer
+   *
+   * The point of this test is to have integration level testing.
+   */
+  public function testCompiledContainerIsDestructed() {
+    $this->enableModules(['system', 'user']);
+    $this->assertNull($this->installConfig('user'));
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRender() {
+    $type = 'processed_text';
+    $element_info = $this->container->get('element_info');
+    $this->assertSame(['#defaults_loaded' => TRUE], $element_info->getInfo($type));
+
+    $this->enableModules(array('filter'));
+
+    $this->assertNotSame($element_info, $this->container->get('element_info'));
+    $this->assertNotEmpty($this->container->get('element_info')->getInfo($type));
+
+    $build = array(
+      '#type' => 'html_tag',
+      '#tag' => 'h3',
+      '#value' => 'Inner',
+    );
+    $expected = "

Inner

\n"; + + $this->assertEquals('core', \Drupal::theme()->getActiveTheme()->getName()); + $output = \Drupal::service('renderer')->renderRoot($build); + $this->assertEquals('core', \Drupal::theme()->getActiveTheme()->getName()); + + $this->assertEquals($expected, $build['#children']); + $this->assertEquals($expected, $output); + } + + /** + * @covers ::render + */ + public function testRenderWithTheme() { + $this->enableModules(array('system')); + + $build = array( + '#type' => 'textfield', + '#name' => 'test', + ); + $expected = '/' . preg_quote('assertArrayNotHasKey('theme', $GLOBALS); + $output = \Drupal::service('renderer')->renderRoot($build); + $this->assertEquals('core', \Drupal::theme()->getActiveTheme()->getName()); + + $this->assertRegExp($expected, (string) $build['#children']); + $this->assertRegExp($expected, (string) $output); + } + +} diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index ac8afbd3ee8..893452596d5 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -20,9 +20,10 @@ function drupal_phpunit_find_extension_directories($scan_directory) { $extensions = array(); $dirs = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($scan_directory, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS)); foreach ($dirs as $dir) { - if (strpos($dir->getPathname(), 'info.yml') !== FALSE) { + if (strpos($dir->getPathname(), '.info.yml') !== FALSE) { // Cut off ".info.yml" from the filename for use as the extension name. - $extensions[substr($dir->getFilename(), 0, -9)] = $dir->getPathInfo()->getRealPath(); + $extensions[substr($dir->getFilename(), 0, -9)] = $dir->getPathInfo() + ->getRealPath(); } } return $extensions; @@ -35,10 +36,19 @@ function drupal_phpunit_find_extension_directories($scan_directory) { * An array of directories under which contributed extensions may exist. */ function drupal_phpunit_contrib_extension_directory_roots() { - $sites_path = __DIR__ . '/../../sites'; - $paths = array(); + $root = dirname(dirname(__DIR__)); + $paths = array( + $root . '/core/modules', + $root . '/core/profiles', + $root . '/modules', + $root . '/profiles', + ); + $sites_path = $root . '/sites'; // Note this also checks sites/../modules and sites/../profiles. foreach (scandir($sites_path) as $site) { + if ($site[0] === '.' || $site === 'simpletest') { + continue; + } $path = "$sites_path/$site"; $paths[] = is_dir("$path/modules") ? realpath("$path/modules") : NULL; $paths[] = is_dir("$path/profiles") ? realpath("$path/profiles") : NULL; @@ -49,37 +59,43 @@ function drupal_phpunit_contrib_extension_directory_roots() { /** * Registers the namespace for each extension directory with the autoloader. * - * @param Composer\Autoload\ClassLoader $loader - * The supplied autoloader. * @param array $dirs * An associative array of extension directories, keyed by extension name. + * + * @return array + * An associative array of extension directories, keyed by their namespace. */ -function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $loader, $dirs) { +function drupal_phpunit_get_extension_namespaces($dirs) { + $namespaces = array(); foreach ($dirs as $extension => $dir) { if (is_dir($dir . '/src')) { // Register the PSR-4 directory for module-provided classes. - $loader->addPsr4('Drupal\\' . $extension . '\\', $dir . '/src'); + $namespaces['Drupal\\' . $extension . '\\'][] = $dir . '/src'; } if (is_dir($dir . '/tests/src')) { // Register the PSR-4 directory for PHPUnit test classes. - $loader->addPsr4('Drupal\\Tests\\' . $extension . '\\', $dir . '/tests/src'); + $namespaces['Drupal\\Tests\\' . $extension . '\\'][] = $dir . '/tests/src'; } } + return $namespaces; } // Start with classes in known locations. $loader = require __DIR__ . '/../../autoload.php'; $loader->add('Drupal\\Tests', __DIR__); +$loader->add('Drupal\\KernelTests', __DIR__); -// Scan for arbitrary extension namespaces from core and contrib. -$extension_roots = array_merge(array( - __DIR__ . '/../modules', - __DIR__ . '/../profiles', -), drupal_phpunit_contrib_extension_directory_roots()); +if (!isset($GLOBALS['namespaces'])) { + // Scan for arbitrary extension namespaces from core and contrib. + $extension_roots = drupal_phpunit_contrib_extension_directory_roots(); -$dirs = array_map('drupal_phpunit_find_extension_directories', $extension_roots); -$dirs = array_reduce($dirs, 'array_merge', array()); -drupal_phpunit_register_extension_dirs($loader, $dirs); + $dirs = array_map('drupal_phpunit_find_extension_directories', $extension_roots); + $dirs = array_reduce($dirs, 'array_merge', array()); + $GLOBALS['namespaces'] = drupal_phpunit_get_extension_namespaces($dirs); +} +foreach ($GLOBALS['namespaces'] as $prefix => $paths) { + $loader->addPsr4($prefix, $paths); +} // Set sane locale settings, to ensure consistent string, dates, times and // numbers handling.