diff --git a/includes/common.inc b/includes/common.inc index 874c3acdcd765bc6b337ed800c94deaaf9651319..8de8894e35e6f6d8fa4ef98f2da7e49a64c7a445 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -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: diff --git a/includes/graph.inc b/includes/graph.inc new file mode 100644 index 0000000000000000000000000000000000000000..38b2abb1c0e8ac5938869ba47506a4854dfb3d7a --- /dev/null +++ b/includes/graph.inc @@ -0,0 +1,143 @@ +<?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; +} + diff --git a/includes/module.inc b/includes/module.inc index c3ee2212873beab6503bd0f5fe7f0c9fa5376ae2..ca5388e575b1c5ac657f17b48bb3c5dff14c2d28 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -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; } diff --git a/modules/simpletest/tests/graph.test b/modules/simpletest/tests/graph.test new file mode 100644 index 0000000000000000000000000000000000000000..ce95ac46b6bc3ea4bfb9e9b13440e34c6e34e688 --- /dev/null +++ b/modules/simpletest/tests/graph.test @@ -0,0 +1,190 @@ +<?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)'; + } + } +} + diff --git a/modules/system/admin.css b/modules/system/admin.css index 00ad19d7a378cbf63a97153dc41a3e53fb12950d..e37c696448afab1428a5df89d7c26d6874cfbee7 100644 --- a/modules/system/admin.css +++ b/modules/system/admin.css @@ -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; } diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index a1f298f65aca8be8d2aa95b5a36df40b9d592507..8aba73ff6f9e1c499a26f592b9ac4dcf4f0baccd 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -527,35 +527,23 @@ function system_theme_settings_submit($form, &$form_state) { * Recursively check compatibility. * * @param $incompatible - * An associative array which at the end of the check contains all incompatible files as the keys, their values being TRUE. + * An associative array which at the end of the check contains all + * incompatible files as the keys, their values being TRUE. * @param $files * The set of files that will be tested. * @param $file * The file at which the check starts. * @return - * Returns TRUE if an incompatible file is found, NULL (no return value) otherwise. + * Returns TRUE if an incompatible file is found, NULL (no return value) + * otherwise. */ function _system_is_incompatible(&$incompatible, $files, $file) { - static $seen; - // We need to protect ourselves in case of a circular dependency. - if (isset($seen[$file->name])) { - return isset($incompatible[$file->name]); - } - $seen[$file->name] = TRUE; if (isset($incompatible[$file->name])) { return TRUE; } - // The 'dependencies' key in .info files was a string in Drupal 5, but changed - // to an array in Drupal 6. If it is not an array, the module is not - // compatible and we can skip the check below which requires an array. - if (!is_array($file->info['dependencies'])) { - $file->info['dependencies'] = array(); - $incompatible[$file->name] = TRUE; - return TRUE; - } - // Recursively traverse the dependencies, looking for incompatible modules - foreach ($file->info['dependencies'] as $dependency) { - if (isset($files[$dependency]) && _system_is_incompatible($incompatible, $files, $files[$dependency])) { + // Recursively traverse required modules, looking for incompatible modules. + foreach ($file->requires as $requires) { + if (isset($files[$requires]) && _system_is_incompatible($incompatible, $files, $files[$requires])) { $incompatible[$file->name] = TRUE; return TRUE; } @@ -566,12 +554,12 @@ function _system_is_incompatible(&$incompatible, $files, $file) { * Menu callback; provides module enable/disable interface. * * The list of modules gets populated by module.info files, which contain each module's name, - * description and dependencies. + * description and information about which modules it requires. * @see drupal_parse_info_file for information on module.info descriptors. * - * Dependency checking is performed to ensure that a module cannot be enabled if the module has - * disabled dependencies and also to ensure that the module cannot be disabled if the module has - * enabled dependents. + * Dependency checking is performed to ensure that a module: + * - can not be enabled if there are disabled modules it requires. + * - can not be disabled if there are enabled modules which depend on it. * * @param $form_state * An associative array containing the current state of the form. @@ -600,14 +588,14 @@ function system_modules($form_state = array()) { uasort($files, 'system_sort_modules_by_info_name'); + // If the modules form was submitted, then system_modules_submit() runs first + // and if there are unfilled required modules, then form_state['storage'] is + // filled, triggering a rebuild. In this case we need to display a + // confirmation form. if (!empty($form_state['storage'])) { - // If the modules form was submitted, then first system_modules_submit runs - // and if there are unfilled dependencies, then form_state['storage'] is - // filled, triggering a rebuild. In this case we need to show a confirm - // form. return system_modules_confirm_form($files, $form_state['storage']); } - $dependencies = array(); + $modules = array(); $form['modules'] = array('#tree' => TRUE); @@ -618,19 +606,17 @@ function system_modules($form_state = array()) { foreach ($files as $filename => $module) { $extra = array(); $extra['enabled'] = (bool) $module->status; - // If this module has dependencies, add them to the array. - if (is_array($module->info['dependencies'])) { - foreach ($module->info['dependencies'] as $dependency) { - if (!isset($files[$dependency])) { - $extra['dependencies'][$dependency] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($dependency))); + // If this module requires other modules, add them to the array. + foreach ($module->requires as $requires => $v) { + if (!isset($files[$requires])) { + $extra['requires'][$requires] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($requires))); $extra['disabled'] = TRUE; } - elseif (!$files[$dependency]->status) { - $extra['dependencies'][$dependency] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$dependency]->info['name'])); + elseif (!$files[$requires]->status) { + $extra['requires'][$requires] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$requires]->info['name'])); } else { - $extra['dependencies'][$dependency] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$dependency]->info['name'])); - } + $extra['requires'][$requires] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$requires]->info['name'])); } } // Generate link for module's help page, if there is one. @@ -640,17 +626,19 @@ function system_modules($form_state = array()) { $extra['help'] = theme('more_help_link', url("admin/help/$filename")); } } - // Mark dependents disabled so user can not remove modules being depended on. + // Mark dependents disabled so the user cannot remove required modules. $dependents = array(); - foreach ($module->info['dependents'] as $dependent) { + // If this module is required by other modules, list those, and then make it + // impossible to disable this one. + foreach ($module->required_by as $required_by => $v) { // Hidden modules are unset already. - if (isset($files[$dependent])) { - if ($files[$dependent]->status == 1) { - $extra['dependents'][] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$dependent]->info['name'])); + if (isset($files[$required_by])) { + if ($files[$required_by]->status == 1) { + $extra['required_by'][] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$required_by]->info['name'])); $extra['disabled'] = TRUE; } else { - $extra['dependents'][] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$dependent]->info['name'])); + $extra['required_by'][] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$required_by]->info['name'])); } } } @@ -694,8 +682,8 @@ function system_sort_modules_by_info_name($a, $b) { function _system_modules_build_row($info, $extra) { // Add in the defaults. $extra += array( - 'dependencies' => array(), - 'dependents' => array(), + 'requires' => array(), + 'required_by' => array(), 'disabled' => FALSE, 'enabled' => FALSE, 'help' => '', @@ -713,8 +701,8 @@ function _system_modules_build_row($info, $extra) { $form['version'] = array( '#markup' => $info['version'], ); - $form['#dependencies'] = $extra['dependencies']; - $form['#dependents'] = $extra['dependents']; + $form['#requires'] = $extra['requires']; + $form['#required_by'] = $extra['required_by']; // Check the compatibilities. $compatible = TRUE; @@ -767,13 +755,13 @@ function _system_modules_build_row($info, $extra) { } /** - * Display confirmation form for dependencies. + * Display confirmation form for required modules. * * @param $modules * Array of module file objects as returned from module_rebuild_cache(). * @param $storage * The contents of $form_state['storage']; an array with two - * elements: the list of dependencies and the list of status + * elements: the list of required modules and the list of status * form field values from the previous screen. * @ingroup forms */ @@ -784,12 +772,12 @@ function system_modules_confirm_form($modules, $storage) { $form['validation_modules'] = array('#type' => 'value', '#value' => $modules); $form['status']['#tree'] = TRUE; - foreach ($storage['dependencies'] as $info) { + foreach ($storage['more_modules'] as $info) { $t_argument = array( '@module' => $info['name'], - '@dependencies' => implode(', ', $info['dependencies']), + '@required' => implode(', ', $info['requires']), ); - $items[] = format_plural(count($info['dependencies']), 'You must enable the @dependencies module to install @module.', 'You must enable the @dependencies modules to install @module.', $t_argument); + $items[] = format_plural(count($info['requires']), 'You must enable the @required module to install @module.', 'You must enable the @required modules to install @module.', $t_argument); } $form['text'] = array('#markup' => theme('item_list', $items)); @@ -826,7 +814,8 @@ function system_modules_submit($form, &$form_state) { $modules = $form_state['storage']['modules']; } - // Get a list of all modules, for building dependencies with. + // Get a list of all modules, it will be used to find which module requires + // which. $files = module_rebuild_cache(); // The modules to be enabled. @@ -835,11 +824,11 @@ function system_modules_submit($form, &$form_state) { $disable_modules = array(); // The modules to be installed. $new_modules = array(); - // The un-met dependencies. - $dependencies = array(); - // Go through each module, finding out - // if we should enable, install, or disable it, - // and if it has any un-met dependencies. + // Modules that need to be switched on because other modules require them. + $more_modules = array(); + // Go through each module, finding out if we should enable, install, or + // disable it. Also, we find out if there are modules it requires that are + // not enabled. foreach ($modules as $name => $module) { // If it's enabled, find out whether to just // enable it, or install it. @@ -850,18 +839,18 @@ function system_modules_submit($form, &$form_state) { elseif (!module_exists($name)) { $modules_to_be_enabled[$name] = $name; } - // If we're not coming from a confirmation form, - // search dependencies. Otherwise, the user will have already - // approved of the dependent modules being enabled. + // If we're not coming from a confirmation form, search for modules the + // new ones require and see whether there are any that additionally + // need to be switched on. if (empty($form_state['storage'])) { - foreach ($form['modules'][$module['group']][$name]['#dependencies'] as $dependency => $string) { - if (!$modules[$dependency]['enabled']) { - if (!isset($dependencies[$name])) { - $dependencies[$name]['name'] = $files[$name]->info['name']; + foreach ($form['modules'][$module['group']][$name]['#requires'] as $requires => $v) { + if (!$modules[$requires]['enabled']) { + if (!isset($more_modules[$name])) { + $more_modules[$name]['name'] = $files[$name]->info['name']; } - $dependencies[$name]['dependencies'][$dependency] = $files[$dependency]->info['name']; + $more_modules[$name]['requires'][$requires] = $files[$requires]->info['name']; } - $modules[$dependency] = array('group' => $files[$dependency]->info['package'], 'enabled' => TRUE); + $modules[$requires] = array('group' => $files[$requires]->info['package'], 'enabled' => TRUE); } } } @@ -873,18 +862,18 @@ function system_modules_submit($form, &$form_state) { $disable_modules[$name] = $name; } } - if ($dependencies) { - // If there where un-met dependencies and they haven't confirmed don't process - // the submission yet. Store the form submission data needed later. + if ($more_modules) { + // If we need to switch on more modules because other modules require + // them and they haven't confirmed, don't process the submission yet. Store + // the form submission data needed later. if (!isset($form_state['values']['confirm'])) { - $form_state['storage'] = array('dependencies' => $dependencies, 'modules' => $modules); + $form_state['storage'] = array('more_modules' => $more_modules, 'modules' => $modules); return; } // Otherwise, install or enable the modules. else { - $dependencies = $form_storage['dependencies']; - foreach ($dependencies as $info) { - foreach ($info['dependencies'] as $dependency => $name) { + foreach ($form_state['storage']['more_modules'] as $info) { + foreach ($info['requires'] as $requires => $name) { if (drupal_get_installed_schema_version($name) == SCHEMA_UNINSTALLED) { $new_modules[$name] = $name; } @@ -895,28 +884,42 @@ function system_modules_submit($form, &$form_state) { } } } - // If we have no dependencies, or the dependencies are confirmed - // to be installed, we don't need the temporary storage anymore. + // Now we have installed every module as required (either by the user or + // because other modules require them) so we don't need the temporary + // storage anymore. unset($form_state['storage']); $old_module_list = module_list(); // Enable the modules needing enabling. if (!empty($modules_to_be_enabled)) { + $sort = array(); + foreach ($modules_to_be_enabled as $module) { + $sort[$module] = $files[$module]->sort; + } + array_multisort($sort, $modules_to_be_enabled); module_enable($modules_to_be_enabled); } // Disable the modules that need disabling. if (!empty($disable_modules)) { + $sort = array(); + foreach ($disable_modules as $module) { + $sort[$module] = $files[$module]->sort; + } + array_multisort($sort, $disable_modules); module_disable($disable_modules); } // Install new modules. if (!empty($new_modules)) { + $sort = array(); foreach ($new_modules as $key => $module) { if (!drupal_check_module($module)) { unset($new_modules[$key]); } + $sort[$module] = $files[$module]->sort; } + array_multisort($sort, $new_modules); drupal_install_modules($new_modules); } @@ -2102,13 +2105,13 @@ function theme_system_modules_fieldset($form) { if (isset($module['help'])) { $description = '<div class="module-help">'. drupal_render($module['help']) .'</div>'; } - // Add the description, along with any dependencies. + // Add the description, along with any modules it requires. $description .= drupal_render($module['description']); - if ($module['#dependencies']) { - $description .= '<div class="admin-dependencies">' . t('Depends on: ') . implode(', ', $module['#dependencies']) . '</div>'; + if ($module['#requires']) { + $description .= '<div class="admin-requirements">' . t('Requires: @module-list', array('@module-list' => implode(', ', $module['#requires']))) . '</div>'; } - if ($module['#dependents']) { - $description .= '<div class="admin-dependencies">' . t('Required by: ') . implode(', ', $module['#dependents']) . '</div>'; + if ($module['#required_by']) { + $description .= '<div class="admin-requirements">' . t('Required by: @module-list', array('@module-list' => implode(', ', $module['#required_by']))) . '</div>'; } $row[] = array('data' => $description, 'class' => 'description'); $rows[] = $row; diff --git a/modules/system/system.test b/modules/system/system.test index d9577f669bf88e1c80ee94dbcb3bd4ae28f2dd2d..f2a684514bcf2ee4a160c21ea2080d5d12bfcdbe 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -1,28 +1,69 @@ <?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 {