Commit a10731ce authored by Dries's avatar Dries

- Patch #320451 by chx, Damien Tournoud: improved Drupal's module dependency...

- Patch #320451 by chx, Damien Tournoud: improved Drupal's module dependency system.  This helps with fields in core.  Comes with tests\!
parent dba81743
......@@ -3866,7 +3866,7 @@ function drupal_write_record($table, &$object, $primary_keys = array()) {
* Information stored in the module.info file:
* - name: The real name of the module for display purposes.
* - description: A brief description of the module.
* - dependencies: An array of shortnames of other modules this module depends on.
* - dependencies: An array of shortnames of other modules this module requires.
* - package: The name of the package of modules this module belongs to.
*
* Example of .info file:
......
<?php
// $Id$
/**
* @file
* Directed acyclic graph functions.
*/
/**
* Perform a depth first sort on a directed acyclic graph.
*
* @param $graph
* A three dimensional associated array, with the first keys being the names
* of the vertices, these can be strings or numbers. The second key is
* 'edges' and the third one are again vertices, each such key representing
* an edge. Values of array elements do not matter.
*
* Example:
* @code
* $graph[1]['edges'][2] = 1;
* $graph[2]['edges'][3] = 1;
* $graph[2]['edges'][4] = 1;
* $graph[3]['edges'][4] = 1;
* @endcode
*
* On return you will also have:
* @code
* $graph[1]['paths'][2] = 1;
* $graph[1]['paths'][3] = 2;
* $graph[2]['reverse_paths'][1] = 1;
* $graph[3]['reverse_paths'][1] = 1;
* @endcode
*
* @return
* The passed in $graph with more secondary keys filled in:
* - 'paths': Contains a list of vertices than can be reached on a path from
* this vertex.
* - 'reverse_paths': Contains a list of vertices that has a path from them
* to this vertex.
* - 'weight': If there is a path from a vertex to another then the weight of
* the latter is higher.
* - 'component': Vertices in the same component have the same component
* identifier.
*
* @see _drupal_depth_first_search()
*/
function drupal_depth_first_search(&$graph) {
$state = array(
// The order of last visit of the depth first search. This is the reverse
// of the topological order if the graph is acyclic.
'last_visit_order' => array(),
// The components of the graph.
'components' => array(),
);
// Perform the actual sort.
foreach ($graph as $start => $data) {
_drupal_depth_first_search($graph, $state, $start);
}
// We do such a numbering that every component starts with 0. This is useful
// for module installs as we can install every 0 weighted module in one
// request, and then every 1 weighted etc.
$component_weights = array();
foreach ($state['last_visit_order'] as $vertex) {
$component = $graph[$vertex]['component'];
if (!isset($component_weights[$component])) {
$component_weights[$component] = 0;
}
$graph[$vertex]['weight'] = $component_weights[$component]--;
}
}
/**
* Helper function to perform a depth first sort.
*
* @param &$graph
* A three dimensional associated graph array.
* @param &$state
* An associative array. The key 'last_visit_order' stores a list of the
* vertices visited. The key components stores list of vertices belonging
* to the same the component.
* @param $start
* An arbitrary vertex where we started traversing the graph.
* @param &$component
* The component of the last vertex.
*
* @see drupal_depth_first_search()
*/
function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL) {
// Assign new component for each new vertex, i.e. when not called recursively.
if (!isset($component)) {
$component = $start;
}
// Nothing to do, if we already visited this vertex.
if (isset($graph[$start]['paths'])) {
return;
}
// Mark $start as visited.
$graph[$start]['paths'] = array();
// Assign $start to the current component.
$graph[$start]['component'] = $component;
$state['components'][$component][] = $start;
// Visit edges of $start.
if (isset($graph[$start]['edges'])) {
foreach ($graph[$start]['edges'] as $end => $v) {
// Mark that $start can reach $end.
$graph[$start]['paths'][$end] = TRUE;
if (isset($graph[$end]['component']) && $component != $graph[$end]['component']) {
// This vertex already has a component, use that from now on and
// reassign all the previously explored vertices.
$new_component = $graph[$end]['component'];
foreach ($state['components'][$component] as $vertex) {
$graph[$vertex]['component'] = $new_component;
$state['components'][$new_component][] = $vertex;
}
unset($state['components'][$component]);
$component = $new_component;
}
// Visit the connected vertex.
_drupal_depth_first_search($graph, $state, $end, $component);
// All vertices reachable by $end are also reachable by $start.
$graph[$start]['paths'] += $graph[$end]['paths'];
}
}
// Now that any other subgraph has been explored, add $start to all reverse
// paths.
foreach ($graph[$start]['paths'] as $end => $v) {
$graph[$end]['reverse_paths'][$start] = TRUE;
}
// Record the order of the last visit. This is the reverse of the
// topological order if the graph is acyclic.
$state['last_visit_order'][] = $start;
}
......@@ -145,69 +145,35 @@ function module_rebuild_cache() {
}
/**
* Find dependencies any level deep and fill in dependents information too.
*
* If module A depends on B which in turn depends on C then this function will
* add C to the list of modules A depends on. This will be repeated until
* module A has a list of all modules it depends on. If it depends on itself,
* called a circular dependency, that's marked by adding a nonexistent module,
* called -circular- to this list of modules. Because this does not exist,
* it'll be impossible to switch module A on.
*
* Also we fill in a dependents array in $file->info. Using the names above,
* the dependents array of module B lists A.
* Find dependencies any level deep and fill in required by information too.
*
* @param $files
* The array of filesystem objects used to rebuild the cache.
* @return
* The same array with dependencies and dependents added where applicable.
* The same array with the new keys for each module:
* - requires: An array with the keys being the modules that this module
* requires.
* - required_by: An array with the keys being the modules that will not work
* without this module.
*/
function _module_build_dependencies($files) {
do {
$new_dependency = FALSE;
foreach ($files as $filename => $file) {
// We will modify this object (module A, see doxygen for module A, B, C).
$file = &$files[$filename];
if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) {
foreach ($file->info['dependencies'] as $dependency_name) {
// This is a nonexistent module.
if ($dependency_name == '-circular-' || !isset($files[$dependency_name])) {
continue;
}
// $dependency_name is module B (again, see doxygen).
$files[$dependency_name]->info['dependents'][$filename] = $filename;
$dependency = $files[$dependency_name];
if (isset($dependency->info['dependencies']) && is_array($dependency->info['dependencies'])) {
// Let's find possible C modules.
foreach ($dependency->info['dependencies'] as $candidate) {
if (array_search($candidate, $file->info['dependencies']) === FALSE) {
// Is this a circular dependency?
if ($candidate == $filename) {
// As a module name can not contain dashes, this makes
// impossible to switch on the module.
$candidate = '-circular-';
// Do not display the message or add -circular- more than once.
if (array_search($candidate, $file->info['dependencies']) !== FALSE) {
continue;
}
drupal_set_message(t('%module is part of a circular dependency. This is not supported and you will not be able to switch it on.', array('%module' => $file->info['name'])), 'error');
}
else {
// We added a new dependency to module A. The next loop will
// be able to use this as "B module" thus finding even
// deeper dependencies.
$new_dependency = TRUE;
}
$file->info['dependencies'][] = $candidate;
}
}
}
}
require_once DRUPAL_ROOT .'/includes/graph.inc';
$roots = $files;
foreach ($files as $filename => $file) {
$graph[$file->name]['edges'] = array();
if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) {
foreach ($file->info['dependencies'] as $dependency_name) {
$graph[$file->name]['edges'][$dependency_name] = 1;
unset($roots[$dependency_name]);
}
// Don't forget to break the reference.
unset($file);
}
} while ($new_dependency);
}
drupal_depth_first_search($graph, array_keys($roots));
foreach ($graph as $module => $data) {
$files[$module]->required_by= isset($data['reverse_paths']) ? $data['reverse_paths'] : array();
$files[$module]->requires = isset($data['paths']) ? $data['paths'] : array();
$files[$module]->sort = $data['weight'];
}
return $files;
}
......
<?php
// $Id$
/**
* @file
* Provides unit tests for graph.inc.
*/
/**
* Unit tests for the graph handling features.
*/
class GraphUnitTest extends DrupalWebTestCase {
function getInfo() {
return array(
'name' => t('Graph'),
'description' => t('Graph handling unit tests.'),
'group' => t('System'),
);
}
/**
* Test depth-first-search features.
*/
function testDepthFirstSearch() {
// Provoke the inclusion of graph.inc.
drupal_function_exists('drupal_depth_first_search');
// The sample graph used is:
// 1 --> 2 --> 3 5 ---> 6
// | ^ ^
// | | |
// | | |
// +---> 4 <-- 7 8 ---> 9
$graph = $this->normalizeGraph(array(
1 => array(2),
2 => array(3, 4),
3 => array(),
4 => array(3),
5 => array(6),
7 => array(4, 5),
8 => array(9),
));
drupal_depth_first_search($graph);
$expected_paths = array(
1 => array(2, 3, 4),
2 => array(3, 4),
3 => array(),
4 => array(3),
5 => array(6),
6 => array(),
7 => array(4, 3, 5, 6),
8 => array(9),
9 => array(),
);
$this->assertPaths($graph, $expected_paths);
$expected_reverse_paths = array(
1 => array(),
2 => array(1),
3 => array(2, 1, 4, 7),
4 => array(2, 1, 7),
5 => array(7),
6 => array(5, 7),
7 => array(),
8 => array(),
9 => array(8),
);
$this->assertReversePaths($graph, $expected_reverse_paths);
$expected_components = array(
array(1, 2, 3, 4, 5, 6, 7),
array(8, 9),
);
$this->assertComponents($graph, $expected_components);
$expected_weights = array(
array(1, 2, 3),
array(2, 4, 3),
array(7, 4, 3),
array(7, 5, 6),
array(8, 9),
);
$this->assertWeights($graph, $expected_weights);
}
/**
* Return a normalized version of a graph.
*/
function normalizeGraph($graph) {
$normalized_graph = array();
foreach ($graph as $vertex => $edges) {
foreach ($edges as $edge) {
$normalized_graph[$vertex]['edges'][$edge] = TRUE;
}
}
return $normalized_graph;
}
/**
* Verify expected paths in a graph.
*
* @param $graph
* A graph array processed by drupal_depth_first_search().
* @param $expected_paths
* An associative array containing vertices with their expected paths.
*/
function assertPaths($graph, $expected_paths) {
foreach ($expected_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = isset($graph[$vertex]['paths']) ? $graph[$vertex]['paths'] : array();
$this->assertEqual($expected, $result, t('Expected paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE))));
}
}
/**
* Verify expected reverse paths in a graph.
*
* @param $graph
* A graph array processed by drupal_depth_first_search().
* @param $expected_reverse_paths
* An associative array containing vertices with their expected reverse
* paths.
*/
function assertReversePaths($graph, $expected_reverse_paths) {
foreach ($expected_reverse_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = isset($graph[$vertex]['reverse_paths']) ? $graph[$vertex]['reverse_paths'] : array();
$this->assertEqual($expected, $result, t('Expected reverse paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE))));
}
}
/**
* Verify expected components in a graph.
*
* @param $graph
* A graph array processed by drupal_depth_first_search().
* @param $expected_components
* An array containing of components defined as a list of their vertices.
*/
function assertComponents($graph, $expected_components) {
$unassigned_vertices = array_fill_keys(array_keys($graph), TRUE);
foreach ($expected_components as $component) {
$result_components = array();
foreach ($component as $vertex) {
$result_components[] = $graph[$vertex]['component'];
unset($unassigned_vertices[$vertex]);
}
$this->assertEqual(1, count(array_unique($result_components)), t('Expected one unique component for vertices @vertices, got @components', array('@vertices' => $this->displayArray($component), '@components' => $this->displayArray($result_components))));
}
$this->assertEqual(array(), $unassigned_vertices, t('Vertices not assigned to a component: @vertices', array('@vertices' => $this->displayArray($unassigned_vertices, TRUE))));
}
/**
* Verify expected order in a graph.
*
* @param $graph
* A graph array processed by drupal_depth_first_search().
* @param $expected_orders
* An array containing lists of vertices in their expected order.
*/
function assertWeights($graph, $expected_orders) {
foreach ($expected_orders as $order) {
$previous_vertex = array_shift($order);
foreach ($order as $vertex) {
$this->assertTrue($graph[$previous_vertex]['weight'] < $graph[$vertex]['weight'], t('Weights of @previous-vertex and @vertex are correct relative to each other', array('@previous-vertex' => $previous_vertex, '@vertex' => $vertex)));
}
}
}
/**
* Helper function to output vertices as comma-separated list.
*
* @param $paths
* An array containing a list of vertices.
* @param $keys
* (optional) Whether to output the keys of $paths instead of the values.
*/
function displayArray($paths, $keys = FALSE) {
if (!empty($paths)) {
return implode(', ', $keys ? array_keys($paths) : $paths);
}
else {
return '(empty)';
}
}
}
......@@ -47,7 +47,7 @@ table.package .description {
table.package .version {
direction: ltr;
}
div.admin-dependencies, div.admin-required {
div.admin-requirements, div.admin-required {
font-size: 0.9em;
color: #444;
}
......
This diff is collapsed.
<?php
// $Id$
class EnableDisableCoreTestCase extends DrupalWebTestCase {
/**
* Helper class for module test cases.
*/
class ModuleTestCase extends DrupalWebTestCase {
protected $admin_user;
function setUp() {
parent::setUp('system_test');
$this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer site configuration'));
$this->drupalLogin($this->admin_user);
}
/**
* Implementation of getInfo().
* Assert there are tables that begin with the specified base table name.
*
* @param $base_table
* Beginning of table name to look for.
* @param $count
* (optional) Whether or not to assert that there are tables that match the
* specified base table. Defaults to TRUE.
*/
function getInfo() {
return array(
'name' => t('Module list functionality'),
'description' => t('Enable/disable core module and confirm table creation/deletion. Enable module without dependency enabled. Attempt disabling of required modules.'),
'group' => t('System')
);
function assertTableCount($base_table, $count = TRUE) {
$tables = db_find_tables(Database::getActiveConnection()->prefixTables('{' . $base_table . '}') . '%');
if ($count) {
return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
}
return $this->assertFalse($tables, t('Tables matching "@base_table" not found.', array('@base_table' => $base_table)));
}
/**
* Implementation of setUp().
* Assert the list of modules are enabled or disabled.
*
* @param $modules
* Module list to check.
* @param $enabled
* Expected module state.
*/
function setUp() {
parent::setUp('system_test');
function assertModules(array $modules, $enabled) {
module_list(TRUE);
foreach ($modules as $module) {
if ($enabled) {
$message = 'Module "@module" is enabled.';
}
else {
$message = 'Module "@module" is not enabled.';
}
$this->assertEqual(module_exists($module), $enabled, t($message, array('@module' => $module)));
}
}
}
$this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer site configuration'));
$this->drupalLogin($this->admin_user);
/**
* Test module enabling/disabling functionality.
*/
class EnableDisableTestCase extends ModuleTestCase {
function getInfo() {
return array(
'name' => t('Enable/disable modules'),
'description' => t('Enable/disable core module and confirm table creation/deletion.'),
'group' => t('Module'),
);
}
/**
......@@ -71,6 +112,19 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
$this->assertModules(array('aggregator'), FALSE);
$this->assertTableCount('aggregator', FALSE);
}
}
/**
* Test module dependency functionality.
*/
class ModuleDependencyTestCase extends ModuleTestCase {
function getInfo() {
return array(
'name' => t('Module dependencies'),
'description' => t('Enable module without dependency enabled.'),
'group' => t('Module'),
);
}
/**
* Attempt to enable translation module without locale enabled.
......@@ -97,6 +151,19 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
$this->assertTableCount('languages', TRUE);
$this->assertTableCount('locale', TRUE);
}
}
/**
* Test required modules functionality.
*/
class ModuleRequiredTestCase extends ModuleTestCase {
function getInfo() {
return array(
'name' => t('Required modules'),
'description' => t('Attempt disabling of required modules.'),
'group' => t('Module'),
);
}
/**
* Assert that core required modules cannot be disabled.
......@@ -109,41 +176,6 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
$this->assertNoFieldByName('modules[Core][' . $module . '][enable]');
}
}
/**
* Assert tables that begin with the specified base table name.
*
* @param string $base_table Beginning of table name to look for.
* @param boolean $count Assert tables that match specified base table.
* @return boolean Tables with specified base table.
*/
function assertTableCount($base_table, $count) {
$tables = db_find_tables(Database::getActiveConnection()->prefixTables('{' . $base_table . '}') . '%');
if ($count) {
return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
}
return $this->assertFalse($tables, t('Tables matching "@base_table" not found.', array('@base_table' => $base_table)));
}
/**
* Assert the list of modules are enabled or disabled.
*
* @param array $modules Modules to check.
* @param boolean $enabled Module state.
*/
function assertModules(array $modules, $enabled) {
module_list(TRUE);
foreach ($modules as $module) {
if ($enabled) {
$message = 'Module "@module" is enabled.';
}
else {
$message = 'Module "@module" is not enabled.';
}
$this->assertEqual(module_exists($module), $enabled, t($message, array('@module' => $module)));
}
}
}
class IPAddressBlockingTestCase extends DrupalWebTestCase {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment