8c92ec7dad4b12cb1687a3ef767c96ba72565cbf
[yaffs-website] / web / core / modules / simpletest / src / KernelTestBase.php
1 <?php
2
3 namespace Drupal\simpletest;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\SafeMarkup;
7 use Drupal\Component\Utility\Variable;
8 use Drupal\Core\Config\Development\ConfigSchemaChecker;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\DrupalKernel;
12 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
13 use Drupal\Core\Extension\ExtensionDiscovery;
14 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
15 use Drupal\Core\Language\Language;
16 use Drupal\Core\Site\Settings;
17 use Symfony\Component\DependencyInjection\Parameter;
18 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
19 use Symfony\Component\DependencyInjection\Reference;
20 use Symfony\Component\HttpFoundation\Request;
21
22 /**
23  * Base class for integration tests.
24  *
25  * Tests extending this base class can access files and the database, but the
26  * entire environment is initially empty. Drupal runs in a minimal mocked
27  * environment, comparable to the one in the early installer.
28  *
29  * The module/hook system is functional and operates on a fixed module list.
30  * Additional modules needed in a test may be loaded and added to the fixed
31  * module list.
32  *
33  * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
34  *   \Drupal\KernelTests\KernelTestBase instead.
35  *
36  * @see \Drupal\simpletest\KernelTestBase::$modules
37  * @see \Drupal\simpletest\KernelTestBase::enableModules()
38  *
39  * @ingroup testing
40  */
41 abstract class KernelTestBase extends TestBase {
42
43   use AssertContentTrait;
44
45   /**
46    * Modules to enable.
47    *
48    * Test classes extending this class, and any classes in the hierarchy up to
49    * this class, may specify individual lists of modules to enable by setting
50    * this property. The values of all properties in all classes in the hierarchy
51    * are merged.
52    *
53    * Any modules specified in the $modules property are automatically loaded and
54    * set as the fixed module list.
55    *
56    * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
57    * automatically installed. Modules need to be installed manually, if needed.
58    *
59    * @see \Drupal\simpletest\KernelTestBase::enableModules()
60    * @see \Drupal\simpletest\KernelTestBase::setUp()
61    *
62    * @var array
63    */
64   public static $modules = [];
65
66   private $moduleFiles;
67   private $themeFiles;
68
69   /**
70    * The configuration directories for this test run.
71    *
72    * @var array
73    */
74   protected $configDirectories = [];
75
76   /**
77    * A KeyValueMemoryFactory instance to use when building the container.
78    *
79    * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
80    */
81   protected $keyValueFactory;
82
83   /**
84    * Array of registered stream wrappers.
85    *
86    * @var array
87    */
88   protected $streamWrappers = [];
89
90   /**
91    * {@inheritdoc}
92    */
93   public function __construct($test_id = NULL) {
94     parent::__construct($test_id);
95     $this->skipClasses[__CLASS__] = TRUE;
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   protected function beforePrepareEnvironment() {
102     // Copy/prime extension file lists once to avoid filesystem scans.
103     if (!isset($this->moduleFiles)) {
104       $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
105       $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
106     }
107   }
108
109   /**
110    * Create and set new configuration directories.
111    *
112    * @see config_get_config_directory()
113    *
114    * @throws \RuntimeException
115    *   Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
116    */
117   protected function prepareConfigDirectories() {
118     $this->configDirectories = [];
119     include_once DRUPAL_ROOT . '/core/includes/install.inc';
120     // Assign the relative path to the global variable.
121     $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
122     $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
123     // Ensure the directory can be created and is writeable.
124     if (!install_ensure_config_directory(CONFIG_SYNC_DIRECTORY)) {
125       throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
126     }
127     // Provide the already resolved path for tests.
128     $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
129   }
130
131   /**
132    * {@inheritdoc}
133    */
134   protected function setUp() {
135     $this->keyValueFactory = new KeyValueMemoryFactory();
136
137     // Back up settings from TestBase::prepareEnvironment().
138     $settings = Settings::getAll();
139
140     // Allow for test-specific overrides.
141     $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
142     $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
143     $container_yamls = [];
144     if (file_exists($settings_services_file)) {
145       // Copy the testing-specific service overrides in place.
146       $testing_services_file = $directory . '/services.yml';
147       copy($settings_services_file, $testing_services_file);
148       $container_yamls[] = $testing_services_file;
149     }
150     $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
151     if (file_exists($settings_testing_file)) {
152       // Copy the testing-specific settings.php overrides in place.
153       copy($settings_testing_file, $directory . '/settings.testing.php');
154     }
155
156     if (file_exists($directory . '/settings.testing.php')) {
157       // Add the name of the testing class to settings.php and include the
158       // testing specific overrides
159       $hash_salt = Settings::getHashSalt();
160       $test_class = get_class($this);
161       $container_yamls_export = Variable::export($container_yamls);
162       $php = <<<EOD
163 <?php
164
165 \$settings['hash_salt'] = '$hash_salt';
166 \$settings['container_yamls'] = $container_yamls_export;
167
168 \$test_class = '$test_class';
169 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
170 EOD;
171       file_put_contents($directory . '/settings.php', $php);
172     }
173
174     // Add this test class as a service provider.
175     // @todo Remove the indirection; implement ServiceProviderInterface instead.
176     $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
177
178     // Bootstrap a new kernel.
179     $class_loader = require DRUPAL_ROOT . '/autoload.php';
180     $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
181     $request = Request::create('/');
182     $site_path = DrupalKernel::findSitePath($request);
183     $this->kernel->setSitePath($site_path);
184     if (file_exists($directory . '/settings.testing.php')) {
185       Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
186     }
187     $this->kernel->boot();
188
189     // Ensure database install tasks have been run.
190     require_once __DIR__ . '/../../../includes/install.inc';
191     $connection = Database::getConnection();
192     $errors = db_installer_object($connection->driver())->runTasks();
193     if (!empty($errors)) {
194       $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
195     }
196
197     // Reboot the kernel because the container might contain a connection to the
198     // database that has been closed during the database install tasks. This
199     // prevents any services created during the first boot from having stale
200     // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
201     $this->kernel->shutdown();
202     $this->kernel->boot();
203
204
205     // Save the original site directory path, so that extensions in the
206     // site-specific directory can still be discovered in the test site
207     // environment.
208     // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
209     $settings['test_parent_site'] = $this->originalSite;
210
211     // Restore and merge settings.
212     // DrupalKernel::boot() initializes new Settings, and the containerBuild()
213     // method sets additional settings.
214     new Settings($settings + Settings::getAll());
215
216     // Create and set new configuration directories.
217     $this->prepareConfigDirectories();
218
219     // Set the request scope.
220     $this->container = $this->kernel->getContainer();
221     $this->container->get('request_stack')->push($request);
222
223     // Re-inject extension file listings into state, unless the key/value
224     // service was overridden (in which case its storage does not exist yet).
225     if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
226       $this->container->get('state')->set('system.module.files', $this->moduleFiles);
227       $this->container->get('state')->set('system.theme.files', $this->themeFiles);
228     }
229
230     // Create a minimal core.extension configuration object so that the list of
231     // enabled modules can be maintained allowing
232     // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
233     // Write directly to active storage to avoid early instantiation of
234     // the event dispatcher which can prevent modules from registering events.
235     \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
236
237     // Collect and set a fixed module list.
238     $class = get_class($this);
239     $modules = [];
240     while ($class) {
241       if (property_exists($class, 'modules')) {
242         // Only add the modules, if the $modules property was not inherited.
243         $rp = new \ReflectionProperty($class, 'modules');
244         if ($rp->class == $class) {
245           $modules[$class] = $class::$modules;
246         }
247       }
248       $class = get_parent_class($class);
249     }
250     // Modules have been collected in reverse class hierarchy order; modules
251     // defined by base classes should be sorted first. Then, merge the results
252     // together.
253     $modules = array_reverse($modules);
254     $modules = call_user_func_array('array_merge_recursive', $modules);
255     if ($modules) {
256       $this->enableModules($modules);
257     }
258
259     // Tests based on this class are entitled to use Drupal's File and
260     // StreamWrapper APIs.
261     // @todo Move StreamWrapper management into DrupalKernel.
262     // @see https://www.drupal.org/node/2028109
263     file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
264     $this->settingsSet('file_public_path', $this->publicFilesDirectory);
265     $this->streamWrappers = [];
266     $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
267     // The temporary stream wrapper is able to operate both with and without
268     // configuration.
269     $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
270
271     // Manually configure the test mail collector implementation to prevent
272     // tests from sending out emails and collect them in state instead.
273     // While this should be enforced via settings.php prior to installation,
274     // some tests expect to be able to test mail system implementations.
275     $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
276   }
277
278   /**
279    * {@inheritdoc}
280    */
281   protected function tearDown() {
282     if ($this->kernel instanceof DrupalKernel) {
283       $this->kernel->shutdown();
284     }
285     // Before tearing down the test environment, ensure that no stream wrapper
286     // of this test leaks into the parent environment. Unlike all other global
287     // state variables in Drupal, stream wrappers are a global state construct
288     // of PHP core, which has to be maintained manually.
289     // @todo Move StreamWrapper management into DrupalKernel.
290     // @see https://www.drupal.org/node/2028109
291     foreach ($this->streamWrappers as $scheme => $type) {
292       $this->unregisterStreamWrapper($scheme, $type);
293     }
294     parent::tearDown();
295   }
296
297   /**
298    * Sets up the base service container for this test.
299    *
300    * Extend this method in your test to register additional service overrides
301    * that need to persist a DrupalKernel reboot. This method is called whenever
302    * the kernel is rebuilt.
303    *
304    * @see \Drupal\simpletest\KernelTestBase::setUp()
305    * @see \Drupal\simpletest\KernelTestBase::enableModules()
306    * @see \Drupal\simpletest\KernelTestBase::disableModules()
307    */
308   public function containerBuild(ContainerBuilder $container) {
309     // Keep the container object around for tests.
310     $this->container = $container;
311
312     // Set the default language on the minimal container.
313     $this->container->setParameter('language.default_values', $this->defaultLanguageData());
314
315     $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
316     $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
317
318     $container
319       ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
320       ->addArgument(Database::getConnection())
321       ->addArgument('config');
322
323     if ($this->strictConfigSchema) {
324       $container
325         ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
326         ->addArgument(new Reference('config.typed'))
327         ->addArgument($this->getConfigSchemaExclusions())
328         ->addTag('event_subscriber');
329     }
330
331     $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
332     $keyvalue_options['default'] = 'keyvalue.memory';
333     $container->setParameter('factory.keyvalue', $keyvalue_options);
334     $container->set('keyvalue.memory', $this->keyValueFactory);
335     if (!$container->has('keyvalue')) {
336       // TestBase::setUp puts a completely empty container in
337       // $this->container which is somewhat the mirror of the empty
338       // environment being set up. Unit tests need not to waste time with
339       // getting a container set up for them. Drupal Unit Tests might just get
340       // away with a simple container holding the absolute bare minimum. When
341       // a kernel is overridden then there's no need to re-register the keyvalue
342       // service but when a test is happy with the superminimal container put
343       // together here, it still might a keyvalue storage for anything using
344       // \Drupal::state() -- that's why a memory service was added in the first
345       // place.
346       $container->register('settings', 'Drupal\Core\Site\Settings')
347         ->setFactoryClass('Drupal\Core\Site\Settings')
348         ->setFactoryMethod('getInstance');
349
350       $container
351         ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
352         ->addArgument(new Reference('service_container'))
353         ->addArgument(new Parameter('factory.keyvalue'));
354
355       $container->register('state', 'Drupal\Core\State\State')
356         ->addArgument(new Reference('keyvalue'));
357     }
358
359     if ($container->hasDefinition('path_processor_alias')) {
360       // Prevent the alias-based path processor, which requires a url_alias db
361       // table, from being registered to the path processor manager. We do this
362       // by removing the tags that the compiler pass looks for. This means the
363       // url generator can safely be used within tests.
364       $definition = $container->getDefinition('path_processor_alias');
365       $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
366     }
367
368     if ($container->hasDefinition('password')) {
369       $container->getDefinition('password')->setArguments([1]);
370     }
371
372     // Register the stream wrapper manager.
373     $container
374       ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
375       ->addArgument(new Reference('module_handler'))
376       ->addMethodCall('setContainer', [new Reference('service_container')]);
377
378     $request = Request::create('/');
379     $container->get('request_stack')->push($request);
380   }
381
382   /**
383    * Provides the data for setting the default language on the container.
384    *
385    * @return array
386    *   The data array for the default language.
387    */
388   protected function defaultLanguageData() {
389     return Language::$defaultValues;
390   }
391
392   /**
393    * Installs default configuration for a given list of modules.
394    *
395    * @param array $modules
396    *   A list of modules for which to install default configuration.
397    *
398    * @throws \RuntimeException
399    *   Thrown when any module listed in $modules is not enabled.
400    */
401   protected function installConfig(array $modules) {
402     foreach ($modules as $module) {
403       if (!$this->container->get('module_handler')->moduleExists($module)) {
404         throw new \RuntimeException("'$module' module is not enabled");
405       }
406       \Drupal::service('config.installer')->installDefaultConfig('module', $module);
407     }
408     $this->pass(format_string('Installed default config: %modules.', [
409       '%modules' => implode(', ', $modules),
410     ]));
411   }
412
413   /**
414    * Installs a specific table from a module schema definition.
415    *
416    * @param string $module
417    *   The name of the module that defines the table's schema.
418    * @param string|array $tables
419    *   The name or an array of the names of the tables to install.
420    *
421    * @throws \RuntimeException
422    *   Thrown when $module is not enabled or when the table schema cannot be
423    *   found in the module specified.
424    */
425   protected function installSchema($module, $tables) {
426     // drupal_get_module_schema() is technically able to install a schema
427     // of a non-enabled module, but its ability to load the module's .install
428     // file depends on many other factors. To prevent differences in test
429     // behavior and non-reproducible test failures, we only allow the schema of
430     // explicitly loaded/enabled modules to be installed.
431     if (!$this->container->get('module_handler')->moduleExists($module)) {
432       throw new \RuntimeException("'$module' module is not enabled");
433     }
434
435     $tables = (array) $tables;
436     foreach ($tables as $table) {
437       $schema = drupal_get_module_schema($module, $table);
438       if (empty($schema)) {
439         // BC layer to avoid some contrib tests to fail.
440         // @todo Remove the BC layer before 8.1.x release.
441         // @see https://www.drupal.org/node/2670360
442         // @see https://www.drupal.org/node/2670454
443         if ($module == 'system') {
444           continue;
445         }
446         throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
447       }
448       $this->container->get('database')->schema()->createTable($table, $schema);
449     }
450     $this->pass(format_string('Installed %module tables: %tables.', [
451       '%tables' => '{' . implode('}, {', $tables) . '}',
452       '%module' => $module,
453     ]));
454   }
455
456
457
458   /**
459    * Installs the storage schema for a specific entity type.
460    *
461    * @param string $entity_type_id
462    *   The ID of the entity type.
463    */
464   protected function installEntitySchema($entity_type_id) {
465     /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
466     $entity_manager = $this->container->get('entity.manager');
467     $entity_type = $entity_manager->getDefinition($entity_type_id);
468     $entity_manager->onEntityTypeCreate($entity_type);
469
470     // For test runs, the most common storage backend is a SQL database. For
471     // this case, ensure the tables got created.
472     $storage = $entity_manager->getStorage($entity_type_id);
473     if ($storage instanceof SqlEntityStorageInterface) {
474       $tables = $storage->getTableMapping()->getTableNames();
475       $db_schema = $this->container->get('database')->schema();
476       $all_tables_exist = TRUE;
477       foreach ($tables as $table) {
478         if (!$db_schema->tableExists($table)) {
479           $this->fail(SafeMarkup::format('Installed entity type table for the %entity_type entity type: %table', [
480             '%entity_type' => $entity_type_id,
481             '%table' => $table,
482           ]));
483           $all_tables_exist = FALSE;
484         }
485       }
486       if ($all_tables_exist) {
487         $this->pass(SafeMarkup::format('Installed entity type tables for the %entity_type entity type: %tables', [
488           '%entity_type' => $entity_type_id,
489           '%tables' => '{' . implode('}, {', $tables) . '}',
490         ]));
491       }
492     }
493   }
494
495   /**
496    * Enables modules for this test.
497    *
498    * To install test modules outside of the testing environment, add
499    * @code
500    * $settings['extension_discovery_scan_tests'] = TRUE;
501    * @endcode
502    * to your settings.php.
503    *
504    * @param array $modules
505    *   A list of modules to enable. Dependencies are not resolved; i.e.,
506    *   multiple modules have to be specified with dependent modules first.
507    *   The new modules are only added to the active module list and loaded.
508    */
509   protected function enableModules(array $modules) {
510     // Perform an ExtensionDiscovery scan as this function may receive a
511     // profile that is not the current profile, and we don't yet have a cached
512     // way to receive inactive profile information.
513     // @todo Remove as part of https://www.drupal.org/node/2186491
514     $listing = new ExtensionDiscovery(\Drupal::root());
515     $module_list = $listing->scan('module');
516     // In ModuleHandlerTest we pass in a profile as if it were a module.
517     $module_list += $listing->scan('profile');
518     // Set the list of modules in the extension handler.
519     $module_handler = $this->container->get('module_handler');
520
521     // Write directly to active storage to avoid early instantiation of
522     // the event dispatcher which can prevent modules from registering events.
523     $active_storage = \Drupal::service('config.storage');
524     $extensions = $active_storage->read('core.extension');
525
526     foreach ($modules as $module) {
527       $module_handler->addModule($module, $module_list[$module]->getPath());
528       // Maintain the list of enabled modules in configuration.
529       $extensions['module'][$module] = 0;
530     }
531     $active_storage->write('core.extension', $extensions);
532
533     // Update the kernel to make their services available.
534     $module_filenames = $module_handler->getModuleList();
535     $this->kernel->updateModules($module_filenames, $module_filenames);
536
537     // Ensure isLoaded() is TRUE in order to make
538     // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
539     // Note that the kernel has rebuilt the container; this $module_handler is
540     // no longer the $module_handler instance from above.
541     $this->container->get('module_handler')->reload();
542     $this->pass(format_string('Enabled modules: %modules.', [
543       '%modules' => implode(', ', $modules),
544     ]));
545   }
546
547   /**
548    * Disables modules for this test.
549    *
550    * @param array $modules
551    *   A list of modules to disable. Dependencies are not resolved; i.e.,
552    *   multiple modules have to be specified with dependent modules first.
553    *   Code of previously active modules is still loaded. The modules are only
554    *   removed from the active module list.
555    */
556   protected function disableModules(array $modules) {
557     // Unset the list of modules in the extension handler.
558     $module_handler = $this->container->get('module_handler');
559     $module_filenames = $module_handler->getModuleList();
560     $extension_config = $this->config('core.extension');
561     foreach ($modules as $module) {
562       unset($module_filenames[$module]);
563       $extension_config->clear('module.' . $module);
564     }
565     $extension_config->save();
566     $module_handler->setModuleList($module_filenames);
567     $module_handler->resetImplementations();
568     // Update the kernel to remove their services.
569     $this->kernel->updateModules($module_filenames, $module_filenames);
570
571     // Ensure isLoaded() is TRUE in order to make
572     // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
573     // Note that the kernel has rebuilt the container; this $module_handler is
574     // no longer the $module_handler instance from above.
575     $module_handler = $this->container->get('module_handler');
576     $module_handler->reload();
577     $this->pass(format_string('Disabled modules: %modules.', [
578       '%modules' => implode(', ', $modules),
579     ]));
580   }
581
582   /**
583    * Registers a stream wrapper for this test.
584    *
585    * @param string $scheme
586    *   The scheme to register.
587    * @param string $class
588    *   The fully qualified class name to register.
589    * @param int $type
590    *   The Drupal Stream Wrapper API type. Defaults to
591    *   StreamWrapperInterface::NORMAL.
592    */
593   protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
594     $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
595   }
596
597   /**
598    * Renders a render array.
599    *
600    * @param array $elements
601    *   The elements to render.
602    *
603    * @return string
604    *   The rendered string output (typically HTML).
605    */
606   protected function render(array &$elements) {
607     // Use the bare HTML page renderer to render our links.
608     $renderer = $this->container->get('bare_html_page_renderer');
609     $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
610
611     // Glean the content from the response object.
612     $content = $response->getContent();
613     $this->setRawContent($content);
614     $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
615     return $content;
616   }
617
618 }