=== added file 'includes/graph.inc' --- includes/graph.inc 1970-01-01 00:00:00 +0000 +++ includes/graph.inc 2008-09-29 11:56:33 +0000 @@ -0,0 +1,279 @@ +_list[$id])) { + $this->_list[$id] = array(); + $this->_list[$id][Graph::GRAPH_LINKS] = array(); + } + return $this; + } + + /** + * Adds a link between two node ids. + * + * @param String $from_id + * The start point of the link. + * @param String $to_id + * The end point of the link. + * @return Graph + */ + public function addLink( $from_id, $to_id) { + $this->add( $from_id); + $this->add( $to_id); + $this->_addLink($from_id, $to_id); + return $this; + } + + /** + * Implementation of bidirection links between the given node ids. + * + * @param string $from_id + * @param string $to_id + */ + protected function _addLink( $from_id, $to_id) { + if (!in_array($to_id, $this->getLinks($from_id))) { + $this->_list[$from_id][Graph::GRAPH_LINKS][] = $to_id; + } + if (!in_array($from_id, $this->getLinks($to_id))) { + $this->_list[$to_id][Graph::GRAPH_LINKS][] = $from_id; + } + } + + /** + * Adds a list of links to a node. + * + * @param string $from_id + * @param array $to_ids + * + * @return Graph + */ + public function addLinks( $from_id, $to_ids) { + foreach ($to_ids as $to_id) { + $this->addLink( $from_id, $to_id); + } + return $this; + } + + /** + * Returns all links from the give node id. + * + * @param string $from_id + * @return array of node ids leaving the given node id. + */ + public function getLinks( $id) { + return array_values($this->_list[$id][Graph::GRAPH_LINKS]); + } + + /** + * Gives all participants related to the given node(s). + * + * @param array $list + * The list of Ids interested in. + * @return array + * All Ids related to the given list. + */ + public function getParticipants( $list= array()) { + if (empty($list)) { + return array_keys($this->_list); + } + $visited = array(); + $agenda = array_values($list); + while ($id= array_shift($agenda)) { + // Prevent infinite looping + if (!isset($visited[$id])) { + $visited[$id]= TRUE; + $agenda= array_merge( $agenda, $this->getLinks($id)); + } + } + return array_keys($visited); + } + public function isCircularMember( $id) { + $route = $this->getParticipants(array($id)); + foreach ($route as $visited_id) { + if (in_array($id, $this->getLinks($visited_id))) { + return TRUE; + } + } + return FALSE; + } +} + +class DirectedGraph extends Graph { + const GRAPH_ROOT = '_root'; + // Top level node id. + private $_root= DirectedGraph::GRAPH_ROOT; + + /** + * Implementation of uni directed link between two nodes + * + * @see addLink() + * + * @param string $from_id + * @param string $to_id + */ + protected function _addLink( $from_id, $to_id) { + if (!in_array($to_id, $this->_list[$from_id][Graph::GRAPH_LINKS])) { + $this->_list[$from_id][Graph::GRAPH_LINKS][] = $to_id; + } + } + + /** + * Builds a reversed graph. + * + * All unidirectional links are reversed. + * + * @return DirectedGraph + */ + public function getReversedGraph() { + $result = new DirectedGraph(); + foreach (array_keys($this->_list) as $from_id) { + $result->add($from_id); + foreach ($this->getLinks($from_id) as $to_id) { + $result->addLink($to_id, $from_id); + } + } + return $result; + } + + protected function getRoot() { + return $this->_root; + } + + /** + * Adds a root element to the graph. + * + * All elements not linked to will be linked to by a root. + * + * @see getTSL() + * + * @param unknown_type $id + */ + protected function addRoot() { + $g = $this->getReversedGraph(); + foreach ($g->_list as $key => $data) { + if (empty( $data[Graph::GRAPH_LINKS]) && ($key!==$this->_root)) { + $this->addLink($this->_root, $key); + } + } + } + + /** + * A subgraph is calculated based on the participants collected based on the given node ids. + * + * @param array $ids + * The nodes interested in. + * @return DirectedGraph + * The subgraph with all participants + */ + public function subGraph( $ids = array()) { + $g = new DirectedGraph(); + $participants= $this->getParticipants( $ids); + foreach ($participants as $id) { + $g->add($id); + // Only participating links are added. + $g->addLinks($id, array_intersect($participants, $this->getLinks($id))); + } + return $g; + } + + /** + * Calculates the Topological Sorted List. + * + * A Topological Sorted List is a Depth First Search ordered + * list of participants. + * + * TODO: Do we need a Directed Acyclic Graph? + * If there are cycles/loops then the algorithme does not loop forever. + * But the TSL is not really a TSL. + * + * The algorithme is based on the Iterator example from + * the book Higher Order Perl where a recusive function + * can be rewritten into a loop. + * + * @param array $ids + * List of nodes interested in. + * @return array + * The TSL ordered list of participants + */ + public function getTSL( $ids = array()) { + $g= $this->subGraph($ids); + // By adding a root the DFS is more cleaner/predictable for tests + $g->addRoot(); + $agenda= array($g->getRoot()); + $visited= array(); + $tsl= array(); + while ($inspect= array_pop($agenda)) { + if (!isset($visited[$inspect])) { + $visited[$inspect]= TRUE; + $links = $g->getLinks($inspect); + if (!empty($links)) { + array_push($agenda, $inspect); + //$agenda = array_merge( $agenda, array_diff( $links, array_keys( $visited))); + foreach ($links as $id) { + if (!isset($visited[$id])) { + $agenda[]= $id; + } + } + } + else { + // We are done with this node. + $tsl[]= $inspect; + } + } + else { + // Already inspected so spit it out. + $tsl[]= $inspect; + } + } + return array_diff($tsl, array( $g->getRoot())); + } +} \ No newline at end of file === added file 'includes/module.admin.inc' --- includes/module.admin.inc 1970-01-01 00:00:00 +0000 +++ includes/module.admin.inc 2008-09-29 11:56:33 +0000 @@ -0,0 +1,108 @@ +getTSL(); +} + +/** + * 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 + * Indicating module is part of a circular dependency. + */ +function module_is_part_of_circular( $module) { + return _module_full_graph(TRUE)->isCircularMember($module); +} + +/** + * 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 DirectedGraph + */ +function module_graph($modules = array(), $enabling = TRUE) { + return _module_full_graph($enabling)->subGraph($modules); +} + +/** + * 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) { + return module_graph($modules, $enabling)->getParticipants(); +} + +/** + * Returns the full graph of all modules. + * + * This graph is later used for subgraphs and reverse dependencies. + * + * We saveguard the static variable by returning a clone of the graph. + * + * @return DirectedGraph + */ +function _module_full_graph($enabling) { + drupal_autoload_class('DirectedGraph'); + static $full_graph_enabling; + static $full_graph_disabling; + if (!isset($full_graph_enabling)) { + $all_modules = module_rebuild_cache(); + $full_graph = new DirectedGraph(); + foreach ($all_modules as $module => $data) { + $full_graph->add( $module); + $full_graph->addLinks($module, $data->info['dependencies']); + } + $full_graph_disabling = $full_graph->getReversedGraph(); + } + // SafeGuard the static by returning a clone. + if ($enabling) { + return $full_graph->subGraph(); + } + else { + return $full_graph_disabling->subGraph(); + } +} \ No newline at end of file === modified file 'includes/module.inc' --- includes/module.inc 2008-09-27 19:03:30 +0000 +++ includes/module.inc 2008-09-29 11:56:33 +0000 @@ -132,75 +132,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; + } } /** @@ -269,6 +212,13 @@ * An array of module names. */ function module_enable($module_list) { + // TODO: hack for making tests run. + require_once 'module.admin.inc'; + require_once 'graph.inc'; + + drupal_function_exists('module_get_tsl'); + $module_list = module_get_tsl( $module_list); + $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)); @@ -306,6 +256,9 @@ * An array of module names. */ function module_disable($module_list) { + drupal_function_exists('module_get_tsl'); + $module_list = module_get_tsl( $module_list, FALSE); + $invoke_modules = array(); foreach ($module_list as $module) { if (module_exists($module)) { @@ -484,4 +437,4 @@ */ function drupal_required_modules() { return array('block', 'filter', 'node', 'system', 'user'); -} +} \ No newline at end of file === added file 'modules/simpletest/tests/graph.test' --- modules/simpletest/tests/graph.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/graph.test 2008-09-29 11:56:33 +0000 @@ -0,0 +1,131 @@ + t('Graph Tests'), + 'description' => t('Test graph.inc API.'), + 'group' => t('Graph'), + ); + } + + function testGraphs() { + // Simple Grapsh b -- a -- c -- b + $g = new Graph(); + $g->addLink('b', 'a')->addLink('c', 'a')->addLink('c', 'b'); + $tests = array(); + $tests['Graph->getLinks(a)'] = array( + 'expected' => array( 'b', 'c'), + 'result' => $g->getLinks('a'), + 'sort_result' => TRUE, + ); + $tests['Graph->getParticipants()'] = array( + 'expected' => array( 'a', 'b', 'c'), + 'result' => $g->getParticipants(), + 'sort_result' => TRUE, + ); + $tests['Graph->getParticipants(a)'] = array( + 'expected' => array( 'a', 'b', 'c'), + 'result' => $g->getParticipants(array('a')), + 'sort_result' => TRUE, + ); + // Now same graph as a DirectedGraph + $h= new DirectedGraph(); + $h->addLink('b', 'a')->addLink('c', 'a')->addLink('c', 'b'); + $tests['DirectedGraph->getLinks(a)'] = array( + 'expected' => array(), + 'result' => $h->getLinks('a'), + 'sort_result' => TRUE, + ); + $tests['DirectedGraph->getParticipants'] = array( + 'expected' => array( 'a', 'b', 'c'), + 'result' => $h->getParticipants(), + 'sort_result' => TRUE, + ); + $tests['DirectedGraph->getParticipants(a)'] = array( + 'expected' => array('a'), + 'result' => $h->getParticipants(array('a')), + 'sort_result' => TRUE, + ); + $tests['DirectedGraph->subgraph(b)'] = array( + 'expected' => array('a', 'b'), + 'result' => $h->subGraph(array('b'))->getParticipants(), + 'sort_result' => TRUE, + ); + // Check test results + foreach ($tests as $test => $data) { + $expected = $data['expected']; + $result = $data['result']; + if (isset($data['sort_result']) && $data['sort_result']) { + asort($result); + $result= array_values($result); + } + $this->assertTrue( $result===$expected, t("@m gives result '@r'", array( '@m' => $test, '@r' => implode(', ', $result)))); + } + } + + function testTSL() { + $g= new DirectedGraph(); + $g->addLink('a', 'b')->addLink('b', 'c')->addLink('c', 'd'); + $g->addLink('e', 'f')->addLink('f', 'c'); + $g->addLink('x', 'y')->addLink('y', 'z'); + $g->addLink('p', 'q')->addLink('q', 'r')->addLink('r', 'p'); + + $tests = array(); + $tests['TSL (a)'] = array( + 'expected' => array('d', 'c', 'b', 'a'), + 'result' => $g->getTSL(array('a')), + ); + $tests['Reversed TSL (c)'] = array( + 'expected' => array('a', 'b', 'e', 'f', 'c'), + 'expected_alt' => array('e', 'f', 'a', 'b', 'c'), + 'result' => $g->getReversedGraph()->getTSL(array('c')), + ); + $tests['Subgraph TSL (a)'] = array( + 'expected' => $g->getTSL(array('a')), + 'result' => $g->subGraph(array('a'))->getTSL(), + ); + $tests['TSL (z)'] = array( + 'expected' => array('z'), + 'result' => $g->getTSL(array('z')), + ); + $tests['Reversed TSL (z)'] = array( + 'expected' => array('x', 'y', 'z'), + 'result' => $g->getReversedGraph()->getTSL(array('z')), + ); + } + function testCircularMember() { + $g= new DirectedGraph(); + // Non circular part of graph + $g->addLink('x', 'y')->addLink('y', 'z'); + // Circular part of graph + $g->addLink('p', 'q')->addLink('q', 'r')->addLink('r', 'p'); + $tests['Is circular member (p)'] = array( + 'expected' => TRUE, + 'result' => $g->isCircularMember('p'), + ); + $tests['Is circular member (x)'] = array( + 'expected' => FALSE, + 'result' => $g->isCircularMember('x'), + ); + foreach ($tests as $test => $data) { + $expected = $data['expected']; + $result = $data['result']; + if (isset($data['expected_alt']) && $result!==$expected) { + $expected= $data['expected_alt']; + } + $this->assertTrue( $result===$expected, t("@t", array( '@t' => $test))); + } + } +} === added directory 'modules/simpletest/tests/module' === added file 'modules/simpletest/tests/module.test' --- modules/simpletest/tests/module.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/module.test 2008-09-29 11:56:33 +0000 @@ -0,0 +1,208 @@ + 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'); + drupal_function_exists('module_get_tsl'); + } + + /** + * 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_b_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_values( 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->assertTrue($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() { + // Non circular + $module = 'module_a_test'; + $circular = module_is_part_of_circular($module); + $this->assertFalse( $circular, "'$module' is not part of a circular dependency"); + // Circular + $module = 'module_x_test'; + $circular = module_is_part_of_circular($module); + $this->assertTrue( $circular, "'$module' is part of a circular dependency"); + } +} === added file 'modules/simpletest/tests/module/module_a_test.info' --- modules/simpletest/tests/module/module_a_test.info 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/module/module_a_test.info 2008-09-29 11:56:33 +0000 @@ -0,0 +1,8 @@ +; $Id$ +name = "Module Test A" +description = "Support module for module.inc tests." +core = 7.x +package = Testing +files[] = module_a_test.module +version = VERSION +hidden = TRUE === added file 'modules/simpletest/tests/module/module_a_test.module' --- modules/simpletest/tests/module/module_a_test.module 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/module/module_a_test.module 2008-09-29 11:56:33 +0000 @@ -0,0 +1,5 @@ +