Index: includes/module.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/module.inc,v retrieving revision 1.124 diff -u -r1.124 module.inc --- includes/module.inc 21 Aug 2008 19:36:36 -0000 1.124 +++ includes/module.inc 19 Sep 2008 20:30:05 -0000 @@ -131,75 +131,18 @@ db_query("INSERT INTO {system} (name, info, type, filename, status, bootstrap) VALUES ('%s', '%s', '%s', '%s', %d, %d)", $file->name, serialize($files[$filename]->info), 'module', $file->filename, 0, $bootstrap); } } - $files = _module_build_dependencies($files); + _module_build_dependencies($files); return $files; } -/** - * 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. - * - * @param $files - * The array of filesystem objects used to rebuild the cache. - * @return - * The same array with dependencies and dependents added where applicable. - */ -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; - } - } - } - } +function _module_build_dependencies(&$files) { + foreach ($files as $module_name => $module) { + foreach ($module->info['dependencies'] as $dep_module) { + if (!in_array($module_name, $files[$dep_module]->info['dependents'])) { + $files[$dep_module]->info['dependents'][] = $module_name; } - // Don't forget to break the reference. - unset($file); } - } while ($new_dependency); - return $files; + } } /** @@ -271,6 +214,9 @@ * An array of module names. */ function module_enable($module_list) { + // make sure all dependencies are added + $module_list = module_get_tsl($module_list, TRUE); + $invoke_modules = array(); foreach ($module_list as $module) { $existing = db_fetch_object(db_query("SELECT status FROM {system} WHERE type = '%s' AND name = '%s'", 'module', $module)); @@ -308,6 +254,9 @@ * An array of module names. */ function module_disable($module_list) { + // make sure all dependents are added + $module_list = module_get_tsl($module_list, FALSE); + $invoke_modules = array(); foreach ($module_list as $module) { if (module_exists($module)) { @@ -487,3 +436,209 @@ function drupal_required_modules() { return array('block', 'filter', 'node', 'system', 'user'); } + +/** + * Returns Topological Sorted List of dependant module based on a list of modules. + * + * A Topological Sorted List is calculated based on the graph the modules + * are depending on. + * + * When enabling lists -dependant on- modules. + * When disabling lists -reverse dependant on- modules. + * + * @param array $module + * List of modules to check for + * @param boolean $enabling + * Direction of the list + * + * @return + * A TSL of modules in the direction requested without '-root-'. + */ +function module_get_tsl($modules=array(), $enabling=TRUE) { + $result = _module_get_tsl($modules, $enabling); + return array_diff($result, array('-root-')); +} + +/** + * @see module_get_tsl + * + * @param array $module + * List of modules to check for + * @param boolean $enabling + * Direction of the list + * + * @return + * A TSL of modules in the direction requested. + */ +function _module_get_tsl($modules=array(), $enabling=TRUE) { + $graph = _module_graph($modules, $enabling); + return _module_graph_tsl($graph); +} + +/** + * Checks whether the list of needed modules to enable or disable the given module overlaps. + * + * If the list of modules to get $module enabled has more in common + * with the list of modules to get the $module disabled then the + * given module there is a circular dependency. + * + * @param string $module + * The module name to test for + * @return boolean + * Indication of module is part of a circular dependency. + */ +function module_is_part_of_circular( $module) { + $modules = array($module); + $enabling_tsl = module_get_tsl($modules); + $disabling_tsl = module_get_tsl($modules, FALSE); + + $intersection = array_intersect($enabling_tsl, $disabling_tsl); + $difference = array_diff($intersection, $modules); + return !(empty($difference)); +} + +/** + * Calculates a (part of) the graph of modules dependency relations. + * + * @param array $modules + * List of modules interested in. + * @param boolean $enabling + * Indicates the direction interested in. + * @return array of arrays + * The calculated graph. + */ +function module_graph($modules = array(), $enabling = TRUE) { + $modules = _module_graph($modules, $enabling); + unset( $modules['-root-']); + return $modules; +} + +/** + * @see module_graph + * + * @param array $modules + * List of modules interested in. + * @param boolean $enabling + * Indicates the direction interested in. + * @return array of arrays + * The calculated graph. + */ +function _module_graph($modules = array(), $enabling = TRUE) { + $modules = _module_participants($modules, $enabling); + + $direction = $enabling ? 'dependencies' : 'dependents'; + + $graph = array(); + // Build graph based on direction + foreach ($modules as $key => $module) { + if (!isset($graph[$key])) { + $graph[$key] = array(); + } + $graph[$key] = $module->info[$direction]; + } + // Find all modules referenced to. + $deps = array(); + foreach ($graph as $key => $refs) { + $deps = array_merge( $deps, $refs); + } + $deps = array_unique($deps); + // Root all independant modules + $graph['-root-'] = array_diff( array_keys($graph), $deps); + if (in_array('-missing-', $deps)) { + $graph['-missing-'] = array(); + } + return $graph; +} + +/** + * Calculates all modules necessary for enabling or disabling. + * + * Missing modules have a dependency with '-missing-' so we + * can check for missing modules quite easy. + * + * @param array $modules + * List of modules interested in. + * @param boolean $enabling + * Indicates the direction interested in. + * @return array + * List of all modules needed. + */ +function _module_participants($modules = array(), $enabling = TRUE) { + static $all_modules; + if (!isset($all_modules)) { + $all_modules = module_rebuild_cache(); + } + + if (empty($modules)) { + $modules = array_keys($all_modules); + } + + $direction = $enabling ? 'dependencies' : 'dependents'; + $result = array(); + $deps = array_values($modules); + while (count($deps)) { + $module_name = array_shift($deps); + if (!key_exists($module_name, $result)) { + if (isset($all_modules[$module_name])) { + $module = $all_modules[$module_name]; + $result[$module_name] = $module; + $deps = array_merge( $deps ,$module->info[$direction]); + } + else { + // A missing module is 'created' + $result[$module_name] = (object) array( 'info' => array($direction => array('-missing-'))); + } + } + } + return $result; +} + +/** + * Calculate the Topological Sorted List based on the graph given. + * + * When C depends on B depends on A + * and D depends on B then + * a TSL collects the deepest elements first. + * C,D,B,A and D,C,B,A are solutions + * + * @param array of arrays $graph + * Describes the list of connection. + * @return array + * Contains the node names in Depth First order + */ +function _module_graph_tsl( $graph) { + $result = array(); + _module_graph_df(null, null, TRUE); + foreach (array_keys($graph) as $key) { + $result = _module_graph_df($graph, $key); + } + return _module_graph_df(null, null, TRUE); +} + +/** + * The gives node is seached for deepest children first + * + * @param array of arrays $graph + * @param string $node + * @param boolean $reset + * @return + * When resetting the last result is returned + */ +function _module_graph_df( $graph, $node, $reset = FALSE) { + static $visited; + static $result; + if (!isset( $visited)||$reset) { + $visited = array(); + $last_result = $result; + $result = array(); + return $last_result; + } + if (!isset($visited[$node])) { + $visited[$node]=TRUE; + foreach ($graph[$node] as $_node) { + _module_graph_df($graph, $_node); + } + array_push($result, $node); + } + return null; +} \ No newline at end of file Index: modules/simpletest/tests/module.test =================================================================== RCS file: modules/simpletest/tests/module.test diff -N modules/simpletest/tests/module.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/module.test 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,204 @@ + t('Module Tests'), + 'description' => t('Test module.inc API.'), + 'group' => t('Module'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + // Enable dummy module. + parent::setUp('module_a_test'); + } + + /** + * Checks the module enabled status. + * + * @param string $module + * @return boolean + * Indicates the enabled status of the given module. + */ + function moduleIsEnabled( $module) { + $existing = db_fetch_object(db_query("SELECT status FROM {system} WHERE type = '%s' AND name = '%s'", 'module', $module)); + return is_object($existing) && !($existing->status == 0); + } + + function modulesAreEnabled( $modules) { + foreach ($modules as $module) { + if (!$this->moduleIsEnabled($module)) return FALSE; + } + return TRUE; + } + + function modulesAreDisabled( $modules) { + foreach ($modules as $module) { + if ($this->moduleIsEnabled($module)) return FALSE; + } + return TRUE; + } + + /** + * Test for a working module_enable. + * + * This is for testing that the normal setup is functional. We intend to + * refactor module.inc so here we make sure it's still in op. This is of + * course not the case... breaking module.inc while render the whole test infra + * useless. Anyway this test should be here. + */ + function testModuleAIsEnabled() { + $this->assertTrue($this->moduleIsEnabled('module_a_test'), 'module_a_test is enabled by setup.'); + } + + /** + * Test module enabling. + * + * Modules C and D depends on Module B which is dependent on module A. + */ + function testDependingModule() { + $this->assertTrue($this->moduleIsEnabled('module_a_test'), 'A is enabled by setup.'); + $this->assertFalse($this->moduleIsEnabled('module_b_test'), 'B is disabled before enabling.'); + // simple enabling + module_enable( array('module_b_test')); + $this->assertTrue($this->moduleIsEnabled('module_b_test'), 'B is enabled.'); + // dependent disabling + module_disable(array('module_a_test')); + $this->assertFalse($this->moduleIsEnabled('module_b_test'), + 'Disabling A leads to a dependent disabled B.' + ); + // group enabling + module_enable( array('module_a_test', 'module_b_test')); + $this->assertTrue($this->modulesAreEnabled(array('module_a_test', 'module_b_test')), 'A and B are enabled.'); + // group disabling + module_disable(array('module_a_test', 'module_b_test')); + $this->assertTrue($this->modulesAreDisabled(array('module_a_test', 'module_b_test')), 'A and B are disabled.'); + // depending enabling + module_enable(array('module_c_test', 'module_d_test')); + $this->assertTrue($this->modulesAreEnabled(array('module_a_test', 'module_b_test', 'module_c_test', 'module_d_test')), + 'Enabling C and D needs A and B enabled too.' + ); + // cascading disabling + module_disable(array('module_a_test')); + $this->assertTrue($this->modulesAreDisabled(array('module_a_test', 'module_b_test', 'module_c_test', 'module_d_test')), + 'Disabling A needs disabled B, C and D.' + ); + } + + function testTSL() { + $tests = array(); + + $tests[] = array( + 'modules' => array('module_a_test'), + 'expected' => array('module_a_test'), + 'direction' => 'enabling', + ); + $tests[] = array( + 'modules' => array('module_b_test'), + 'expected' => array('module_a_test', 'module_b_test'), + 'direction' => 'enabling', + ); + $tests[] = array( + 'modules' => array('module_c_test'), + 'expected' => array('module_a_test', 'module_b_test', 'module_c_test'), + 'direction' => 'enabling', + ); + $tests[] = array( + 'modules' => array('module_a_test'), + 'expected' => array('module_c_test', 'module_d_test', 'module_b_test', 'module_a_test'), + 'expected_alt' => array('module_d_test', 'module_c_test', 'module_c_test', 'module_a_test'), + 'direction' => 'disabling', + ); + + foreach ($tests as $test) { + $modules = $test['modules']; + $direction = $test['direction']; + $expected = $test['expected']; + $result = module_get_tsl($modules, $direction=='enabling'); + if (isset($test['expected_alt']) && $result!=$expected) { + $expected=$test['expected_alt']; + } + $this->assertEqual($result, $expected, + t("TSL @d '@m' results in @r.", + array( + "@d" => $direction, + "@m" => implode(",", $modules), + "@r" => implode(",", $result), + ) + ) + ); + } + } + + function testParticipants() { + $all_modules = _module_participants(); + $core_modules = drupal_required_modules(); + $this->assertTrue(count($all_modules)>=count($core_modules), + "There are more modules then the few drupal required ones." + ); + $tests = array(); + $tests[] = array( + 'modules' => array('module_a_test'), + 'expected' => array('module_a_test'), + 'direction' => 'enabling', + ); + $tests[] = array( + 'modules' => array('module_a_test'), + 'expected' => array('module_a_test', 'module_b_test', 'module_c_test', 'module_d_test'), + 'direction' => 'disabling', + ); + $tests[] = array( + 'modules' => array('module_d_test'), + 'expected' => array('module_a_test', 'module_b_test', 'module_d_test'), + 'direction' => 'enabling', + ); + $tests[] = array( + 'modules' => array('module_d_test', 'module_c_test'), + 'expected' => array('module_a_test', 'module_b_test', 'module_c_test', 'module_d_test'), + 'direction' => 'enabling', + ); + foreach ($tests as $test) { + $modules = $test['modules']; + $direction = $test['direction']; + $expected = $test['expected']; + $result = array_keys( _module_participants($modules, $direction=='enabling')); + asort($result); + asort($expected); + // skipping the next two lines renders the assertEqual useless for some tests + $result = array_values($result); + $expected = array_values($expected); + $this->assertEqual($result, $expected, + t("_module_participants for @m when @d gave '@r' versus '@e'", + array( + "@d" => $direction, + "@m" => implode(",", $modules), + "@r" => implode(",", $result), + "@e" => implode(",", $expected), + ) + ) + ); + } + } + + function testCircular() { + $circular = module_is_part_of_circular('module_a_test'); + $this->assertFalse( $circular, 'module_a_test is not part of a circular dependency'); + + $circular = module_is_part_of_circular('module_x_test'); + $this->assertTrue( $circular, 'module_x_test is part of a circular dependency'); + } +} Index: modules/simpletest/tests/module/module_z_test.module =================================================================== RCS file: modules/simpletest/tests/module/module_z_test.module diff -N modules/simpletest/tests/module/module_z_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/module/module_z_test.module 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,5 @@ +