diff --git a/core/includes/graph.inc b/core/includes/graph.inc deleted file mode 100644 index 9ef86a1..0000000 --- a/core/includes/graph.inc +++ /dev/null @@ -1,145 +0,0 @@ - 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]--; - } -} - -/** - * Performs a depth-first sort on a graph. - * - * @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] = $v; - - 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; - } - // Only visit existing vertices. - if (isset($graph[$end])) { - // 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) { - if (isset($graph[$end])) { - $graph[$end]['reverse_paths'][$start] = $v; - } - } - - // 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/core/includes/module.inc b/core/includes/module.inc index df9c138..04023ca 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -5,6 +5,8 @@ * API for loading and interacting with Drupal modules. */ +use Drupal\Component\DepthFirstSearch\DepthFirstSearch; + /** * Load all the modules that have been enabled in the system table. * @@ -240,7 +242,6 @@ function system_list_reset() { * without this module. */ function _module_build_dependencies($files) { - require_once DRUPAL_ROOT . '/core/includes/graph.inc'; foreach ($files as $filename => $file) { $graph[$file->name]['edges'] = array(); if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) { @@ -250,7 +251,8 @@ function _module_build_dependencies($files) { } } } - drupal_depth_first_search($graph); + $depth_first_search = new DepthFirstSearch($graph); + $graph = $depth_first_search->search(); 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(); diff --git a/core/includes/update.inc b/core/includes/update.inc index 7a4a27d..fb0dea7 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -8,6 +8,8 @@ * installation. It is included and used extensively by update.php. */ +use Drupal\Component\DepthFirstSearch\DepthFirstSearch; + /** * Minimum schema version of Drupal 7 required for upgrade to Drupal 8. * @@ -562,7 +564,7 @@ function update_get_update_list() { * * In addition, the returned array also includes detailed information about the * dependency chain for each update, as provided by the depth-first search - * algorithm in drupal_depth_first_search(). + * algorithm in Drupal\Component\DepthFirstSearch\DepthFirstSearch::search(). * * @param $starting_updates * An array whose keys contain the names of modules with updates to be run @@ -575,10 +577,11 @@ function update_get_update_list() { * request, arranged in the order in which the update functions should be * run. (This includes the provided starting update for each module and all * subsequent updates that are available.) The values are themselves arrays - * containing all the keys provided by the drupal_depth_first_search() - * algorithm, which encode detailed information about the dependency chain - * for this update function (for example: 'paths', 'reverse_paths', 'weight', - * and 'component'), as well as the following additional keys: + * containing all the keys provided by the + * Drupal\Component\DepthFirstSearch\DepthFirstSearch::search() algorithm, + * which encode detailed information about the dependency chain for this + * update function (for example: 'paths', 'reverse_paths', 'weight', and + * 'component'), as well as the following additional keys: * - 'allowed': A boolean which is TRUE when the update function's * dependencies are met, and FALSE otherwise. Calling functions should * inspect this value before running the update. @@ -588,7 +591,7 @@ function update_get_update_list() { * - 'module': The name of the module that this update function belongs to. * - 'number': The number of this update function within that module. * - * @see drupal_depth_first_search() + * @see Drupal\Component\DepthFirstSearch\DepthFirstSearch::search() */ function update_resolve_dependencies($starting_updates) { // Obtain a dependency graph for the requested update functions. @@ -596,8 +599,8 @@ function update_resolve_dependencies($starting_updates) { $graph = update_build_dependency_graph($update_functions); // Perform the depth-first search and sort the results. - require_once DRUPAL_ROOT . '/core/includes/graph.inc'; - drupal_depth_first_search($graph); + $depth_first_search = new DepthFirstSearch($graph); + $graph = $depth_first_search->search(); uasort($graph, 'drupal_sort_weight'); foreach ($graph as $function => &$data) { @@ -692,18 +695,20 @@ function update_get_update_function_list($starting_updates) { * * @return * A multidimensional array representing the dependency graph, suitable for - * passing in to drupal_depth_first_search(), but with extra information - * about each update function also included. Each array key contains the name - * of an update function, including all update functions from the provided - * list as well as any outside update functions which they directly depend - * on. Each value is an associative array containing the following keys: + * passing in to Drupal\Component\DepthFirstSearch\DepthFirstSearch::search(), + * but with extra information about each update function also included. Each + * array key contains the name of an update function, including all update + * functions from the provided list as well as any outside update functions + * which they directly depend on. Each value is an associative array + * containing the following keys: * - 'edges': A representation of any other update functions that immediately - * depend on this one. See drupal_depth_first_search() for more details on - * the format. + * depend on this one. See + * Drupal\Component\DepthFirstSearch\DepthFirstSearch::search() for more + * details on the format. * - 'module': The name of the module that this update function belongs to. * - 'number': The number of this update function within that module. * - * @see drupal_depth_first_search() + * @see Drupal\Component\DepthFirstSearch\DepthFirstSearch::search() * @see update_resolve_dependencies() */ function update_build_dependency_graph($update_functions) { diff --git a/core/lib/Drupal/Component/DepthFirstSearch/DepthFirstSearch.php b/core/lib/Drupal/Component/DepthFirstSearch/DepthFirstSearch.php new file mode 100644 index 0000000..2e9da16 --- /dev/null +++ b/core/lib/Drupal/Component/DepthFirstSearch/DepthFirstSearch.php @@ -0,0 +1,162 @@ +graph = $graph; + } + + /** + * Performs a depth-first sort on the directed acyclic graph. + * + * @return + * The given $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. + */ + public function search() { + $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 ($this->graph as $start => $data) { + $this->sort($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 = $this->graph[$vertex]['component']; + if (!isset($component_weights[$component])) { + $component_weights[$component] = 0; + } + $this->graph[$vertex]['weight'] = $component_weights[$component]--; + } + + return $this->graph; + } + + /** + * Performs a depth-first sort on a graph. + * + * @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\Component\DepthFirstSearch\DepthFirstSearch::search() + */ + protected function sort(&$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($this->graph[$start]['paths'])) { + return; + } + // Mark $start as visited. + $this->graph[$start]['paths'] = array(); + + // Assign $start to the current component. + $this->graph[$start]['component'] = $component; + $state['components'][$component][] = $start; + + // Visit edges of $start. + if (isset($this->graph[$start]['edges'])) { + foreach ($this->graph[$start]['edges'] as $end => $v) { + // Mark that $start can reach $end. + $this->graph[$start]['paths'][$end] = $v; + + if (isset($this->graph[$end]['component']) && $component != $this->graph[$end]['component']) { + // This vertex already has a component, use that from now on and + // reassign all the previously explored vertices. + $new_component = $this->graph[$end]['component']; + foreach ($state['components'][$component] as $vertex) { + $this->graph[$vertex]['component'] = $new_component; + $state['components'][$new_component][] = $vertex; + } + unset($state['components'][$component]); + $component = $new_component; + } + // Only visit existing vertices. + if (isset($this->graph[$end])) { + // Visit the connected vertex. + $this->sort($state, $end, $component); + + // All vertices reachable by $end are also reachable by $start. + $this->graph[$start]['paths'] += $this->graph[$end]['paths']; + } + } + } + + // Now that any other subgraph has been explored, add $start to all reverse + // paths. + foreach ($this->graph[$start]['paths'] as $end => $v) { + if (isset($this->graph[$end])) { + $this->graph[$end]['reverse_paths'][$start] = $v; + } + } + + // 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/core/modules/simpletest/tests/graph.test b/core/modules/simpletest/tests/graph.test deleted file mode 100644 index e60cd39..0000000 --- a/core/modules/simpletest/tests/graph.test +++ /dev/null @@ -1,195 +0,0 @@ - 'Graph', - 'description' => 'Graph handling unit tests.', - 'group' => 'System', - ); - } - - function setUp() { - require_once DRUPAL_ROOT . '/core/includes/graph.inc'; - parent::setUp(); - } - - /** - * Test depth-first-search features. - */ - function testDepthFirstSearch() { - // 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), - 9 => array(), - )); - 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), - 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), - 7 => array(), - 8 => array(), - 9 => array(8), - ); - $this->assertReversePaths($graph, $expected_reverse_paths); - - // Assert that DFS didn't created "missing" vertexes automatically. - $this->assertFALSE(isset($graph[6]), t('Vertex 6 has not been created')); - - $expected_components = array( - array(1, 2, 3, 4, 5, 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), - 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) { - // Create vertex even if it hasn't any edges. - $normalized_graph[$vertex] = array(); - 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/core/modules/system/system.test b/core/modules/system/system.test index 099c673..4e8a336 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -1,6 +1,7 @@ assertResponse(404, t("Make sure index.php/user returns a 'page not found'.")); } } + +/** + * Unit tests for the graph handling features. + * + * @see Drupal\Component\DepthFirstSearch\DepthFirstSearch + */ +class DepthFirstSearchUnitTest extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Depth first search', + 'description' => 'Depth first search unit tests.', + 'group' => 'System', + ); + } + + /** + * Test depth-first-search features. + */ + function testDepthFirstSearch() { + // 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), + 9 => array(), + )); + $depth_first_search = new DepthFirstSearch($graph); + $graph = $depth_first_search->search(); + + $expected_paths = array( + 1 => array(2, 3, 4), + 2 => array(3, 4), + 3 => array(), + 4 => array(3), + 5 => array(6), + 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), + 7 => array(), + 8 => array(), + 9 => array(8), + ); + $this->assertReversePaths($graph, $expected_reverse_paths); + + // Assert that DFS didn't created "missing" vertexes automatically. + $this->assertFALSE(isset($graph[6]), t('Vertex 6 has not been created')); + + $expected_components = array( + array(1, 2, 3, 4, 5, 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), + 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) { + // Create vertex even if it hasn't any edges. + $normalized_graph[$vertex] = array(); + 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\Component\DepthFirstSearch\DepthFirstSearch::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\Component\DepthFirstSearch\DepthFirstSearch::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\Component\DepthFirstSearch\DepthFirstSearch::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\Component\DepthFirstSearch\DepthFirstSearch::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)'; + } + } +}