mirror of
https://github.com/NixOS/nix.git
synced 2025-11-09 12:06:01 +01:00
feat(libstore): add findCycles() to DependencyGraph
Adds cycle detection to DependencyGraph using DFS with back-edge detection. This will be used by the cycle detection feature for build errors. Each cycle is represented as a path that starts and ends at the same node, e.g., [A, B, C, A].
This commit is contained in:
parent
60857de63e
commit
56dbca9a98
3 changed files with 123 additions and 0 deletions
|
|
@ -94,4 +94,64 @@ TEST(DependencyGraph, EmptyGraph)
|
||||||
EXPECT_EQ(depGraph.getAllNodes().size(), 0);
|
EXPECT_EQ(depGraph.getAllNodes().size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for cycle detection tests
|
||||||
|
*/
|
||||||
|
struct FindCyclesParams
|
||||||
|
{
|
||||||
|
std::string description;
|
||||||
|
std::vector<std::pair<std::string, std::string>> inputEdges;
|
||||||
|
std::vector<std::vector<std::string>> expectedCycles;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FindCyclesTest : public ::testing::TestWithParam<FindCyclesParams>
|
||||||
|
{};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool compareCycles(const std::vector<std::string> & a, const std::vector<std::string> & b)
|
||||||
|
{
|
||||||
|
if (a.size() != b.size())
|
||||||
|
return a.size() < b.size();
|
||||||
|
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_P(FindCyclesTest, FindCycles)
|
||||||
|
{
|
||||||
|
const auto & params = GetParam();
|
||||||
|
|
||||||
|
FilePathGraph depGraph;
|
||||||
|
for (const auto & [from, to] : params.inputEdges) {
|
||||||
|
depGraph.addEdge(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto actualCycles = depGraph.findCycles();
|
||||||
|
EXPECT_EQ(actualCycles.size(), params.expectedCycles.size());
|
||||||
|
|
||||||
|
std::ranges::sort(actualCycles, compareCycles);
|
||||||
|
auto expectedCycles = params.expectedCycles;
|
||||||
|
std::ranges::sort(expectedCycles, compareCycles);
|
||||||
|
|
||||||
|
EXPECT_EQ(actualCycles, expectedCycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_CASE_P(
|
||||||
|
FindCycles,
|
||||||
|
FindCyclesTest,
|
||||||
|
::testing::Values(
|
||||||
|
FindCyclesParams{"empty input", {}, {}},
|
||||||
|
FindCyclesParams{"single edge no cycle", {{"a", "b"}}, {}},
|
||||||
|
FindCyclesParams{"simple cycle", {{"a", "b"}, {"b", "a"}}, {{"a", "b", "a"}}},
|
||||||
|
FindCyclesParams{"three node cycle", {{"a", "b"}, {"b", "c"}, {"c", "a"}}, {{"a", "b", "c", "a"}}},
|
||||||
|
FindCyclesParams{
|
||||||
|
"four node cycle", {{"a", "b"}, {"b", "c"}, {"c", "d"}, {"d", "a"}}, {{"a", "b", "c", "d", "a"}}},
|
||||||
|
FindCyclesParams{
|
||||||
|
"multiple disjoint cycles",
|
||||||
|
{{"a", "b"}, {"b", "a"}, {"c", "d"}, {"d", "c"}},
|
||||||
|
{{"a", "b", "a"}, {"c", "d", "c"}}},
|
||||||
|
FindCyclesParams{"cycle with extra edges", {{"a", "b"}, {"b", "a"}, {"c", "d"}}, {{"a", "b", "a"}}},
|
||||||
|
FindCyclesParams{"self-loop", {{"a", "a"}}, {{"a", "a"}}},
|
||||||
|
FindCyclesParams{"chain no cycle", {{"a", "b"}, {"b", "c"}, {"c", "d"}}, {}},
|
||||||
|
FindCyclesParams{"cycle with tail", {{"x", "a"}, {"a", "b"}, {"b", "c"}, {"c", "a"}}, {{"a", "b", "c", "a"}}}));
|
||||||
|
|
||||||
} // namespace nix
|
} // namespace nix
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "nix/util/error.hh"
|
#include "nix/util/error.hh"
|
||||||
|
|
||||||
#include <boost/graph/graph_traits.hpp>
|
#include <boost/graph/graph_traits.hpp>
|
||||||
|
#include <boost/graph/depth_first_search.hpp>
|
||||||
#include <boost/graph/reverse_graph.hpp>
|
#include <boost/graph/reverse_graph.hpp>
|
||||||
#include <boost/graph/properties.hpp>
|
#include <boost/graph/properties.hpp>
|
||||||
|
|
||||||
|
|
@ -222,6 +223,61 @@ DependencyGraph<NodeId, EdgeProperty>::getEdgeProperty(const NodeId & from, cons
|
||||||
return graph[edge];
|
return graph[edge];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<GraphNodeId NodeId, typename EdgeProperty>
|
||||||
|
std::vector<std::vector<NodeId>> DependencyGraph<NodeId, EdgeProperty>::findCycles() const
|
||||||
|
{
|
||||||
|
using vertex_descriptor = typename boost::graph_traits<Graph>::vertex_descriptor;
|
||||||
|
using edge_descriptor = typename boost::graph_traits<Graph>::edge_descriptor;
|
||||||
|
|
||||||
|
std::vector<std::vector<vertex_descriptor>> cycleDescriptors;
|
||||||
|
std::vector<vertex_descriptor> dfsPath;
|
||||||
|
|
||||||
|
// Custom DFS visitor to detect back edges and extract cycles
|
||||||
|
class CycleFinder : public boost::default_dfs_visitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::vector<std::vector<vertex_descriptor>> & cycles;
|
||||||
|
std::vector<vertex_descriptor> & dfsPath;
|
||||||
|
|
||||||
|
CycleFinder(std::vector<std::vector<vertex_descriptor>> & cycles, std::vector<vertex_descriptor> & dfsPath)
|
||||||
|
: cycles(cycles)
|
||||||
|
, dfsPath(dfsPath)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void discover_vertex(vertex_descriptor v, const Graph & g)
|
||||||
|
{
|
||||||
|
dfsPath.push_back(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish_vertex(vertex_descriptor v, const Graph & g)
|
||||||
|
{
|
||||||
|
if (!dfsPath.empty() && dfsPath.back() == v) {
|
||||||
|
dfsPath.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void back_edge(edge_descriptor e, const Graph & g)
|
||||||
|
{
|
||||||
|
auto target = boost::target(e, g);
|
||||||
|
auto cycleStart = std::ranges::find(dfsPath, target);
|
||||||
|
std::vector<vertex_descriptor> cycle(cycleStart, dfsPath.end());
|
||||||
|
cycle.push_back(target);
|
||||||
|
cycles.push_back(std::move(cycle));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CycleFinder visitor(cycleDescriptors, dfsPath);
|
||||||
|
boost::depth_first_search(graph, boost::visitor(visitor));
|
||||||
|
|
||||||
|
// Convert vertex_descriptors to NodeIds using ranges
|
||||||
|
return cycleDescriptors | std::views::transform([&](const auto & cycleVerts) {
|
||||||
|
return cycleVerts | std::views::transform([&](auto v) { return getNodeId(v); })
|
||||||
|
| std::ranges::to<std::vector<NodeId>>();
|
||||||
|
})
|
||||||
|
| std::ranges::to<std::vector>();
|
||||||
|
}
|
||||||
|
|
||||||
template<GraphNodeId NodeId, typename EdgeProperty>
|
template<GraphNodeId NodeId, typename EdgeProperty>
|
||||||
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getAllNodes() const
|
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getAllNodes() const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "nix/util/canon-path.hh"
|
#include "nix/util/canon-path.hh"
|
||||||
|
|
||||||
#include <boost/graph/adjacency_list.hpp>
|
#include <boost/graph/adjacency_list.hpp>
|
||||||
|
#include <boost/graph/depth_first_search.hpp>
|
||||||
#include <boost/graph/dijkstra_shortest_paths.hpp>
|
#include <boost/graph/dijkstra_shortest_paths.hpp>
|
||||||
#include <boost/graph/reverse_graph.hpp>
|
#include <boost/graph/reverse_graph.hpp>
|
||||||
|
|
||||||
|
|
@ -137,6 +138,12 @@ public:
|
||||||
|
|
||||||
std::vector<NodeId> getAllNodes() const;
|
std::vector<NodeId> getAllNodes() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all cycles in the graph using DFS.
|
||||||
|
* Returns vector of cycles, each represented as a path that starts and ends at the same node.
|
||||||
|
*/
|
||||||
|
std::vector<std::vector<NodeId>> findCycles() const;
|
||||||
|
|
||||||
size_t numVertices() const
|
size_t numVertices() const
|
||||||
{
|
{
|
||||||
return boost::num_vertices(graph);
|
return boost::num_vertices(graph);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue