#320451: clean up module dependency checking.

From:  <>


---

 includes/graph.inc                  |  101 ++++++++++++++++++++++++
 includes/module.inc                 |   76 +++++-------------
 modules/simpletest/tests/graph.test |   87 ++++++++++++++++++++
 modules/system/system.admin.inc     |  150 ++++++++++++++++++-----------------
 modules/system/system.test          |  117 ++++++++++++++++-----------
 5 files changed, 353 insertions(+), 178 deletions(-)

diff --git includes/graph.inc includes/graph.inc
new file mode 100644
index 0000000..a8ee35b
--- /dev/null
+++ includes/graph.inc
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * Performs 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. Value does not matter.
+ *   On return, more secondary keys will be filled in:
+ *     'paths' will contain a list of vertices than can be reached on a path
+ *   from this vertex.
+ *     'reverse_paths' will contain 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.
+ *
+ *   Example:
+ *   $graph[1]['edges'][2] = 1;
+ *   $graph[2]['edges'][3] = 1;
+ *   $graph[2]['edges'][4] = 1;
+ *   $graph[3]['edges'][4] = 1;
+ *
+ *   On return you will also have:
+ *   $graph[1]['paths'][2] = 1;
+ *   $graph[1]['paths'][3] = 2;
+ *   $graph[2]['reverse_paths'][1] = 1;
+ *   $graph[3]['reverse_paths'][1] = 1;
+ */
+function drupal_depth_first_search(&$graph) {
+  $last_visit_order = array();
+  foreach ($graph as $start => $data) {
+    $traversing_data = array(
+      'component' => $start,
+    );
+    _drupal_depth_first_search($graph, $last_visit_order, $traversing_data, $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.
+  $weights = array();
+
+  foreach ($last_visit_order as $vertex) {
+    if (!isset($weights[$graph[$vertex]['component']])) {
+      $weights[$graph[$vertex]['component']] = 0;
+    }
+    $graph[$vertex]['weight'] = $weights[$graph[$vertex]['component']]--;
+  }
+}
+
+function _drupal_depth_first_search(&$graph, &$last_visit_order, &$traversing_data, $start) {
+  if (!isset($graph[$start]['paths'])) {
+    // Mark $start as visited.
+    $graph[$start]['paths'] = array();
+
+    // Record that we visited $start in this exploration.
+    $traversing_data['explored'][] = $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'])) {
+          // The other vertice already has a component, use that from now on
+          // and reassign all the previously explored vertices.
+          $traversing_data['component'] = $graph[$end]['component'];
+          foreach ($traversing_data['explored'] as $vertice) {
+            $graph[$vertice]['component'] = $traversing_data['component'];
+          }
+        }
+
+        // Visit the connected vertice.
+        _drupal_depth_first_search($graph, $last_visit_order, $traversing_data, $end);
+
+        // All vertices reachable by $end are also reachable by $start.
+        $graph[$start]['paths'] += $graph[$end]['paths'];
+      }
+    }
+
+    // Mark the component if we don't already did.
+    if (!isset($graph[$start]['component'])) {
+      $graph[$start]['component'] = $traversing_data['component'];
+    }
+
+    // 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 a acyclic.
+    $last_visit_order[] = $start;
+  }
+}
diff --git includes/module.inc includes/module.inc
index 2e7d376..e032c0d 100644
--- includes/module.inc
+++ 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:
+ *   - depends_on: An array with the keys being the modules that this module
+ *     depends on.
+ *   - 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]->depends_on = isset($data['paths']) ? $data['paths'] : array();
+    $files[$module]->sort = $data['weight'];
+  }
   return $files;
 }
 
diff --git modules/simpletest/tests/graph.test modules/simpletest/tests/graph.test
new file mode 100644
index 0000000..7eb8ef2
--- /dev/null
+++ modules/simpletest/tests/graph.test
@@ -0,0 +1,87 @@
+<?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
+             |     ^
+             |     |
+             |     |
+             +---> 4
+    */
+    $graph[1]['edges'][2] = 1;
+    $graph[2]['edges'][3] = 1;
+    $graph[2]['edges'][4] = 1;
+    $graph[4]['edges'][3] = 1;
+
+    drupal_depth_first_search($graph, array(1));
+
+    $this->assertTrue(isset($graph[1]['paths'][2]), t('Path 1-&gt;2 found.'));
+    unset($graph[1]['paths'][2]);
+    $this->assertTrue(isset($graph[1]['paths'][3]), t('Path 1-&gt;3 found.'));
+    unset($graph[1]['paths'][3]);
+    $this->assertTrue(isset($graph[1]['paths'][4]), t('Path 1-&gt;4 found.'));
+    unset($graph[1]['paths'][4]);
+    $this->assertTrue(isset($graph[2]['paths'][3]), t('Path 2-&gt;3 found.'));
+    unset($graph[2]['paths'][3]);
+    $this->assertTrue(isset($graph[2]['paths'][4]), t('Path 2-&gt;4 found.'));
+    unset($graph[2]['paths'][4]);
+    $this->assertTrue(isset($graph[4]['paths'][3]), t('Path 4-&gt;3 found.'));
+    unset($graph[4]['paths'][3]);
+
+    $more_paths = FALSE;
+    foreach ($graph as $vertex => $data) {
+      $more_paths = $more_paths || !empty($data['paths']);
+    }
+    $this->assertFalse($more_paths, t('There are no more paths.'));
+
+    $this->assertTrue(isset($graph[2]['reverse_paths'][1]), t('Reverse path 2-&gt;1 found.'));
+    unset($graph[2]['reverse_paths'][1]);
+    $this->assertTrue(isset($graph[3]['reverse_paths'][1]), t('Reverse path 3-&gt;1 found.'));
+    unset($graph[3]['reverse_paths'][1]);
+    $this->assertTrue(isset($graph[4]['reverse_paths'][1]), t('Reverse path 4-&gt;1 found.'));
+    unset($graph[4]['reverse_paths'][1]);
+    $this->assertTrue(isset($graph[3]['reverse_paths'][2]), t('Reverse path 3-&gt;2 found.'));
+    unset($graph[3]['reverse_paths'][2]);
+    $this->assertTrue(isset($graph[4]['reverse_paths'][2]), t('Reverse path 4-&gt;2 found.'));
+    unset($graph[4]['reverse_paths'][2]);
+    $this->assertTrue(isset($graph[3]['reverse_paths'][4]), t('Reverse path 3-&gt;4 found.'));
+    unset($graph[3]['reverse_paths'][4]);
+
+    $more_reverse_paths = FALSE;
+    foreach ($graph as $vertex => $data) {
+      $more_reverse_paths = $more_reverse_paths || !empty($data['more_reverse_paths']);
+    }
+    $this->assertFalse($more_reverse_paths, t('There are no more reverse paths.'));
+
+    $this->assertTrue($graph[1]['weight'] < $graph[2]['weight'], t('Weighting on 1 and 2 correct relative to each other.'));
+    $this->assertTrue($graph[2]['weight'] < $graph[3]['weight'], t('Weighting on 2 and 3 correct relative to each other.'));
+    $this->assertTrue($graph[2]['weight'] < $graph[4]['weight'], t('Weighting on 2 and 4 correct relative to each other.'));
+    $this->assertTrue($graph[4]['weight'] < $graph[3]['weight'], t('Weighting on 4 and 3 correct relative to each other.'));
+  }
+}
diff --git modules/system/system.admin.inc modules/system/system.admin.inc
index 393fd19..8044183 100644
--- modules/system/system.admin.inc
+++ modules/system/system.admin.inc
@@ -536,26 +536,12 @@ function system_theme_settings_submit($form, &$form_state) {
  *   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 the depends_on, looking for incompatible modules
+  foreach ($file->depends_on as $depends_on) {
+    if (isset($files[$depends_on]) && _system_is_incompatible($incompatible, $files, $files[$depends_on])) {
       $incompatible[$file->name] = TRUE;
       return TRUE;
     }
@@ -566,12 +552,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.
@@ -602,12 +588,11 @@ function system_modules($form_state = array()) {
 
   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
+    // and if there are unfilled depends_on, 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 +603,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 has depends_on, add them to the array.
+    foreach ($module->depends_on as $depends_on => $v) {
+      if (!isset($files[$depends_on])) {
+        $extra['depends_on'][$depends_on] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($depends_on)));
           $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[$depends_on]->status) {
+        $extra['depends_on'][$depends_on] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$depends_on]->info['name']));
         }
         else {
-          $extra['dependencies'][$dependency] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$dependency]->info['name']));
-        }
+        $extra['depends_on'][$depends_on] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$depends_on]->info['name']));
       }
     }
     // Generate link for module's help page, if there is one.
@@ -642,15 +625,17 @@ function system_modules($form_state = array()) {
     }
     // Mark dependents disabled so user can not remove modules being depended on.
     $dependents = array();
-    foreach ($module->info['dependents'] as $dependent) {
+    // If this module is required by others then list those and 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 +679,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(),
+    'depends_on' => array(),
+    'required_by' => array(),
     'disabled' => FALSE,
     'enabled' => FALSE,
     'help' => '',
@@ -713,8 +698,8 @@ function _system_modules_build_row($info, $extra) {
   $form['version'] = array(
     '#markup' => $info['version'],
   );
-  $form['#dependencies'] = $extra['dependencies'];
-  $form['#dependents'] = $extra['dependents'];
+  $form['#depends_on'] = $extra['depends_on'];
+  $form['#required_by'] = $extra['required_by'];
 
   // Check the compatibilities.
   $compatible = TRUE;
@@ -767,13 +752,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 +769,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']),
+      '@depends_on' => implode(', ', $info['depends_on']),
     );
-    $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['depends_on']), 'You must enable the @depends_on module to install @module.', 'You must enable the @depends_on modules to install @module.', $t_argument);
   }
   $form['text'] = array('#markup' => theme('item_list', $items));
 
@@ -826,7 +811,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 depends on
+  // which.
   $files = module_rebuild_cache();
 
   // The modules to be enabled.
@@ -835,11 +821,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 depend on 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 depends on and are
+  // not on.
   foreach ($modules as $name => $module) {
     // If it's enabled, find out whether to just
     // enable it, or install it.
@@ -850,18 +836,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 depend on 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]['#depends_on'] as $depends_on => $v) {
+          if (!$modules[$depends_on]['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]['depends_on'][$depends_on] = $files[$depends_on]->info['name'];
           }
-          $modules[$dependency] = array('group' => $files[$dependency]->info['package'], 'enabled' => TRUE);
+          $modules[$depends_on] = array('group' => $files[$depends_on]->info['package'], 'enabled' => TRUE);
         }
       }
     }
@@ -873,18 +859,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 depend on
+    // 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['depends_on'] as $depends_on => $name) {
           if (drupal_get_installed_schema_version($name) == SCHEMA_UNINSTALLED) {
             $new_modules[$name] = $name;
           }
@@ -895,28 +881,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 depend on 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);
   }
 
@@ -2101,13 +2101,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 this one depends on.
     $description .= drupal_render($module['description']);
-    if ($module['#dependencies']) {
-     $description .= '<div class="admin-dependencies">' . t('Depends on: ') . implode(', ', $module['#dependencies']) . '</div>';
+    if ($module['#depends_on']) {
+     $description .= '<div class="admin-depends_on">' . t('Depends on: ') . implode(', ', $module['#depends_on']) . '</div>';
     }
-    if ($module['#dependents']) {
-     $description .= '<div class="admin-dependencies">' . t('Required by: ') . implode(', ', $module['#dependents']) . '</div>';
+    if ($module['#required_by']) {
+     $description .= '<div class="admin-depends_on">' . t('Required by: ') . implode(', ', $module['#required_by']) . '</div>';
     }
     $row[] = array('data' => $description, 'class' => 'description');
     $rows[] = $row;
diff --git modules/system/system.test modules/system/system.test
index 657a0f8..8c6d658 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -1,28 +1,64 @@
 <?php
 // $Id: system.test,v 1.34 2009-01-02 21:45:11 dries Exp $
 
-class EnableDisableCoreTestCase extends DrupalWebTestCase {
+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 tables that begin with the specified base table name.
+   *
+   * @param $base_table
+   *   Beginning of table name to look for.
+   * @param $count
+   *   Assert tables that match specified base table.
+   * @return
+   *   Tables with specified base table.
    */
-  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) {
+    $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
+   *   Modules to check.
+   * @param $enabled
+   *   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);
+class EnableDisableTestCase extends ModuleTestCase {
+  function getInfo() {
+    return array(
+      'name' => t('Module list functionality'),
+      'description' => t('Enable/disable core module and confirm table creation/deletion.'),
+      'group' => t('Module')
+    );
   }
 
   /**
@@ -71,6 +107,16 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
     $this->assertModules(array('aggregator'), FALSE);
     $this->assertTableCount('aggregator', FALSE);
   }
+}
+
+class ModuleNoDependencyTestCase extends ModuleTestCase {
+  function getInfo() {
+    return array(
+      'name' => t('Disabling required modules'),
+      'description' => t('Attempt disabling of required modules.'),
+      'group' => t('Module')
+    );
+  }
 
   /**
    * Attempt to enable translation module without locale enabled.
@@ -97,6 +143,16 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
     $this->assertTableCount('languages', TRUE);
     $this->assertTableCount('locale', TRUE);
   }
+}
+
+class ModuleNoRequiredTestCase extends ModuleTestCase {
+  function getInfo() {
+    return array(
+      'name' => t('Module list w/o dependency enabled'),
+      'description' => t('Enable module without dependency enabled. Attempt disabling of required modules.'),
+      'group' => t('Module')
+    );
+  }
 
   /**
    * Assert that core required modules cannot be disabled.
@@ -109,41 +165,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 {
