3 namespace Drupal\Tests\Core\Extension;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Extension\Extension;
7 use Drupal\Core\Extension\ModuleHandler;
8 use Drupal\Tests\UnitTestCase;
11 * @coversDefaultClass \Drupal\Core\Extension\ModuleHandler
12 * @runTestsInSeparateProcesses
16 class ModuleHandlerTest extends UnitTestCase {
19 * The mocked cache backend.
21 * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
23 protected $cacheBackend;
28 * @covers ::__construct
30 protected function setUp() {
32 // We can mock the cache handler here, but not the module handler.
33 $this->cacheBackend = $this->getMock(CacheBackendInterface::class);
37 * Get a module handler object to test.
39 * Since we have to run these tests in separate processes, we have to use
40 * test objects which are serializable. Since ModuleHandler will populate
41 * itself with Extension objects, and since Extension objects will try to
42 * access DRUPAL_ROOT when they're unserialized, we can't store our mocked
43 * ModuleHandler objects as a property in unit tests. They must be generated
44 * by the test method by calling this method.
46 * @return \Drupal\Core\Extension\ModuleHandler
47 * The module handler to test.
49 protected function getModuleHandler() {
50 $module_handler = new ModuleHandler($this->root, [
51 'module_handler_test' => [
53 'pathname' => 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml',
54 'filename' => 'module_handler_test.module',
56 ], $this->cacheBackend);
57 return $module_handler;
61 * Test loading a module.
65 public function testLoadModule() {
66 $module_handler = $this->getModuleHandler();
67 $this->assertFalse(function_exists('module_handler_test_hook'));
68 $this->assertTrue($module_handler->load('module_handler_test'));
69 $this->assertTrue(function_exists('module_handler_test_hook'));
71 $module_handler->addModule('module_handler_test_added', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_added');
72 $this->assertFalse(function_exists('module_handler_test_added_hook'), 'Function does not exist before being loaded.');
73 $this->assertTrue($module_handler->load('module_handler_test_added'));
74 $this->assertTrue(function_exists('module_handler_test_added_helper'), 'Function exists after being loaded.');
75 $this->assertTrue($module_handler->load('module_handler_test_added'));
77 $this->assertFalse($module_handler->load('module_handler_test_dne'), 'Non-existent modules returns false.');
81 * Test loading all modules.
85 public function testLoadAllModules() {
86 $module_handler = $this->getModuleHandler();
87 $module_handler->addModule('module_handler_test_all1', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all1');
88 $module_handler->addModule('module_handler_test_all2', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all2');
89 $this->assertFalse(function_exists('module_handler_test_all1_hook'), 'Function does not exist before being loaded.');
90 $this->assertFalse(function_exists('module_handler_test_all2_hook'), 'Function does not exist before being loaded.');
91 $module_handler->loadAll();
92 $this->assertTrue(function_exists('module_handler_test_all1_hook'), 'Function exists after being loaded.');
93 $this->assertTrue(function_exists('module_handler_test_all2_hook'), 'Function exists after being loaded.');
101 public function testModuleReloading() {
102 $module_handler = $this->getMockBuilder(ModuleHandler::class)
103 ->setConstructorArgs([
106 'module_handler_test' => [
108 'pathname' => 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml',
109 'filename' => 'module_handler_test.module',
111 ], $this->cacheBackend
113 ->setMethods(['load'])
116 $module_handler->expects($this->at(0))
118 ->with($this->equalTo('module_handler_test'));
120 $module_handler->expects($this->at(1))
122 ->with($this->equalTo('module_handler_test'));
123 $module_handler->expects($this->at(2))
125 ->with($this->equalTo('module_handler_test_added'));
126 $module_handler->reload();
127 $module_handler->addModule('module_handler_test_added', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_added');
128 $module_handler->reload();
132 * Test isLoaded accessor.
136 public function testIsLoaded() {
137 $module_handler = $this->getModuleHandler();
138 $this->assertFalse($module_handler->isLoaded());
139 $module_handler->loadAll();
140 $this->assertTrue($module_handler->isLoaded());
144 * Confirm we get back the modules set in the constructor.
146 * @covers ::getModuleList
148 public function testGetModuleList() {
149 $this->assertEquals($this->getModuleHandler()->getModuleList(), [
150 'module_handler_test' => new Extension($this->root, 'module', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml', 'module_handler_test.module'),
155 * Confirm we get back a module from the module list
157 * @covers ::getModule
159 public function testGetModuleWithExistingModule() {
160 $this->assertEquals($this->getModuleHandler()->getModule('module_handler_test'), new Extension($this->root, 'module', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml', 'module_handler_test.module'));
164 * @covers ::getModule
166 public function testGetModuleWithNonExistingModule() {
167 $this->setExpectedException(\InvalidArgumentException::class);
168 $this->getModuleHandler()->getModule('claire_alice_watch_my_little_pony_module_that_does_not_exist');
172 * Ensure setting the module list replaces the module list and resets internal structures.
174 * @covers ::setModuleList
176 public function testSetModuleList() {
177 $fixture_module_handler = $this->getModuleHandler();
178 $module_handler = $this->getMockBuilder(ModuleHandler::class)
179 ->setConstructorArgs([
180 $this->root, [], $this->cacheBackend
182 ->setMethods(['resetImplementations'])
185 // Ensure we reset implementations when settings a new modules list.
186 $module_handler->expects($this->once())->method('resetImplementations');
188 // Make sure we're starting empty.
189 $this->assertEquals($module_handler->getModuleList(), []);
191 // Replace the list with a prebuilt list.
192 $module_handler->setModuleList($fixture_module_handler->getModuleList());
194 // Ensure those changes are stored.
195 $this->assertEquals($fixture_module_handler->getModuleList(), $module_handler->getModuleList());
199 * Test adding a module.
201 * @covers ::addModule
204 public function testAddModule() {
206 $module_handler = $this->getMockBuilder(ModuleHandler::class)
207 ->setConstructorArgs([
208 $this->root, [], $this->cacheBackend
210 ->setMethods(['resetImplementations'])
213 // Ensure we reset implementations when settings a new modules list.
214 $module_handler->expects($this->once())->method('resetImplementations');
216 $module_handler->addModule('module_handler_test', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test');
217 $this->assertTrue($module_handler->moduleExists('module_handler_test'));
221 * Test adding a profile.
223 * @covers ::addProfile
226 public function testAddProfile() {
228 $module_handler = $this->getMockBuilder(ModuleHandler::class)
229 ->setConstructorArgs([
230 $this->root, [], $this->cacheBackend
232 ->setMethods(['resetImplementations'])
235 // Ensure we reset implementations when settings a new modules list.
236 $module_handler->expects($this->once())->method('resetImplementations');
238 // @todo this should probably fail since its a module not a profile.
239 $module_handler->addProfile('module_handler_test', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test');
240 $this->assertTrue($module_handler->moduleExists('module_handler_test'));
244 * Test module exists returns correct module status.
246 * @covers ::moduleExists
248 public function testModuleExists() {
249 $module_handler = $this->getModuleHandler();
250 $this->assertTrue($module_handler->moduleExists('module_handler_test'));
251 $this->assertFalse($module_handler->moduleExists('module_handler_test_added'));
255 * @covers ::loadAllIncludes
257 public function testLoadAllIncludes() {
258 $this->assertTrue(TRUE);
259 $module_handler = $this->getMockBuilder(ModuleHandler::class)
260 ->setConstructorArgs([
263 'module_handler_test' => [
265 'pathname' => 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml',
266 'filename' => 'module_handler_test.module',
268 ], $this->cacheBackend
270 ->setMethods(['loadInclude'])
273 // Ensure we reset implementations when settings a new modules list.
274 $module_handler->expects($this->once())->method('loadInclude');
275 $module_handler->loadAllIncludes('hook');
279 * @covers ::loadInclude
281 * Note we load code, so isolate the test.
283 * @runInSeparateProcess
284 * @preserveGlobalState disabled
286 public function testLoadInclude() {
287 $module_handler = $this->getModuleHandler();
289 $this->assertEquals(__DIR__ . '/modules/module_handler_test/hook_include.inc', $module_handler->loadInclude('module_handler_test', 'inc', 'hook_include'));
290 $this->assertTrue(function_exists('module_handler_test_hook_include'));
291 // Include doesn't exist.
292 $this->assertFalse($module_handler->loadInclude('module_handler_test', 'install'));
296 * Test invoke methods when module is enabled.
300 public function testInvokeModuleEnabled() {
301 $module_handler = $this->getModuleHandler();
302 $this->assertTrue($module_handler->invoke('module_handler_test', 'hook', [TRUE]), 'Installed module runs hook.');
303 $this->assertFalse($module_handler->invoke('module_handler_test', 'hook', [FALSE]), 'Installed module runs hook.');
304 $this->assertNull($module_handler->invoke('module_handler_test_fake', 'hook', [FALSE]), 'Installed module runs hook.');
308 * Test implementations methods when module is enabled.
310 * @covers ::implementsHook
311 * @covers ::loadAllIncludes
313 public function testImplementsHookModuleEnabled() {
314 $module_handler = $this->getModuleHandler();
315 $this->assertTrue($module_handler->implementsHook('module_handler_test', 'hook'), 'Installed module implementation found.');
317 $module_handler->addModule('module_handler_test_added', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_added');
318 $this->assertTrue($module_handler->implementsHook('module_handler_test_added', 'hook'), 'Runtime added module with implementation in include found.');
320 $module_handler->addModule('module_handler_test_no_hook', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_added');
321 $this->assertFalse($module_handler->implementsHook('module_handler_test_no_hook', 'hook', [TRUE]), 'Missing implementation not found.');
325 * Test getImplementations.
327 * @covers ::getImplementations
328 * @covers ::getImplementationInfo
329 * @covers ::buildImplementationInfo
331 public function testGetImplementations() {
332 $this->assertEquals(['module_handler_test'], $this->getModuleHandler()->getImplementations('hook'));
336 * Test getImplementations.
338 * @covers ::getImplementations
339 * @covers ::getImplementationInfo
341 public function testCachedGetImplementations() {
342 $this->cacheBackend->expects($this->exactly(1))
344 ->will($this->onConsecutiveCalls(
345 (object) ['data' => ['hook' => ['module_handler_test' => 'test']]]
348 // Ensure buildImplementationInfo doesn't get called and that we work off cached results.
349 $module_handler = $this->getMockBuilder(ModuleHandler::class)
350 ->setConstructorArgs([
352 'module_handler_test' => [
354 'pathname' => 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml',
355 'filename' => 'module_handler_test.module',
357 ], $this->cacheBackend
359 ->setMethods(['buildImplementationInfo', 'loadInclude'])
361 $module_handler->load('module_handler_test');
363 $module_handler->expects($this->never())->method('buildImplementationInfo');
364 $module_handler->expects($this->once())->method('loadInclude');
365 $this->assertEquals(['module_handler_test'], $module_handler->getImplementations('hook'));
369 * Test getImplementations.
371 * @covers ::getImplementations
372 * @covers ::getImplementationInfo
374 public function testCachedGetImplementationsMissingMethod() {
375 $this->cacheBackend->expects($this->exactly(1))
377 ->will($this->onConsecutiveCalls((object) [
380 'module_handler_test' => [],
381 'module_handler_test_missing' => [],
386 // Ensure buildImplementationInfo doesn't get called and that we work off cached results.
387 $module_handler = $this->getMockBuilder(ModuleHandler::class)
388 ->setConstructorArgs([
390 'module_handler_test' => [
392 'pathname' => 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test/module_handler_test.info.yml',
393 'filename' => 'module_handler_test.module',
395 ], $this->cacheBackend
397 ->setMethods(['buildImplementationInfo'])
399 $module_handler->load('module_handler_test');
401 $module_handler->expects($this->never())->method('buildImplementationInfo');
402 $this->assertEquals(['module_handler_test'], $module_handler->getImplementations('hook'));
408 * @covers ::invokeAll
410 public function testInvokeAll() {
411 $module_handler = $this->getModuleHandler();
412 $module_handler->addModule('module_handler_test_all1', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all1');
413 $module_handler->addModule('module_handler_test_all2', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all2');
414 $this->assertEquals([TRUE, TRUE, TRUE], $module_handler->invokeAll('hook', [TRUE]));
418 * Test that write cache calls through to cache library correctly.
420 * @covers ::writeCache
422 public function testWriteCache() {
423 $module_handler = $this->getModuleHandler();
425 ->expects($this->exactly(2))
427 ->will($this->returnValue(NULL));
429 ->expects($this->exactly(2))
431 ->with($this->logicalOr('module_implements', 'hook_info'));
432 $module_handler->getImplementations('hook');
433 $module_handler->writeCache();
437 * Test hook_hook_info() fetching through getHookInfo().
439 * @covers ::getHookInfo
440 * @covers ::buildHookInfo
442 public function testGetHookInfo() {
443 $module_handler = $this->getModuleHandler();
444 // Set up some synthetic results.
446 ->expects($this->exactly(2))
448 ->will($this->onConsecutiveCalls(
450 (object) ['data' => ['hook_foo' => ['group' => 'hook']]]
453 // Results from building from mocked environment.
454 $this->assertEquals([
455 'hook' => ['group' => 'hook'],
456 ], $module_handler->getHookInfo());
458 // Reset local cache so we get our synthetic result from the cache handler.
459 $module_handler->resetImplementations();
460 $this->assertEquals([
461 'hook_foo' => ['group' => 'hook'],
462 ], $module_handler->getHookInfo());
466 * Test internal implementation cache reset.
468 * @covers ::resetImplementations
470 public function testResetImplementations() {
471 $module_handler = $this->getModuleHandler();
473 $module_handler->getImplementations('hook');
474 $module_handler->getHookInfo();
476 // Reset all caches internal and external.
478 ->expects($this->once())
482 ->expects($this->exactly(2))
484 // reset sets module_implements to array() and getHookInfo later
485 // populates hook_info.
486 ->with($this->logicalOr('module_implements', 'hook_info'));
487 $module_handler->resetImplementations();
489 // Request implementation and ensure hook_info and module_implements skip
492 ->expects($this->exactly(2))
494 ->with($this->logicalOr('module_implements', 'hook_info'));
495 $module_handler->getImplementations('hook');
499 * @dataProvider dependencyProvider
500 * @covers ::parseDependency
502 public function testDependencyParsing($dependency, $expected) {
503 $version = ModuleHandler::parseDependency($dependency);
504 $this->assertEquals($expected, $version);
508 * Provider for testing dependency parsing.
510 public function dependencyProvider() {
512 ['system', ['name' => 'system']],
513 ['taxonomy', ['name' => 'taxonomy']],
514 ['views', ['name' => 'views']],
515 ['views_ui(8.x-1.0)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.0)', 'versions' => [['op' => '=', 'version' => '1.0']]]],
517 // array('views_ui(8.x-1.1-beta)', array('name' => 'views_ui', 'original_version' => ' (8.x-1.1-beta)', 'versions' => array(array('op' => '=', 'version' => '1.1-beta')))),
518 ['views_ui(8.x-1.1-alpha12)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.1-alpha12)', 'versions' => [['op' => '=', 'version' => '1.1-alpha12']]]],
519 ['views_ui(8.x-1.1-beta8)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.1-beta8)', 'versions' => [['op' => '=', 'version' => '1.1-beta8']]]],
520 ['views_ui(8.x-1.1-rc11)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.1-rc11)', 'versions' => [['op' => '=', 'version' => '1.1-rc11']]]],
521 ['views_ui(8.x-1.12)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.12)', 'versions' => [['op' => '=', 'version' => '1.12']]]],
522 ['views_ui(8.x-1.x)', ['name' => 'views_ui', 'original_version' => ' (8.x-1.x)', 'versions' => [['op' => '<', 'version' => '2.x'], ['op' => '>=', 'version' => '1.x']]]],
523 ['views_ui( <= 8.x-1.x)', ['name' => 'views_ui', 'original_version' => ' ( <= 8.x-1.x)', 'versions' => [['op' => '<=', 'version' => '2.x']]]],
524 ['views_ui(<= 8.x-1.x)', ['name' => 'views_ui', 'original_version' => ' (<= 8.x-1.x)', 'versions' => [['op' => '<=', 'version' => '2.x']]]],
525 ['views_ui( <=8.x-1.x)', ['name' => 'views_ui', 'original_version' => ' ( <=8.x-1.x)', 'versions' => [['op' => '<=', 'version' => '2.x']]]],
526 ['views_ui(>8.x-1.x)', ['name' => 'views_ui', 'original_version' => ' (>8.x-1.x)', 'versions' => [['op' => '>', 'version' => '2.x']]]],
527 ['drupal:views_ui(>8.x-1.x)', ['project' => 'drupal', 'name' => 'views_ui', 'original_version' => ' (>8.x-1.x)', 'versions' => [['op' => '>', 'version' => '2.x']]]],
532 * @covers ::getModuleDirectories
534 public function testGetModuleDirectories() {
535 $module_handler = $this->getModuleHandler();
536 $module_handler->setModuleList([]);
537 $module_handler->addModule('module', 'place');
538 $this->assertEquals(['module' => $this->root . '/place'], $module_handler->getModuleDirectories());