Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/libstore-tests/dependency-graph.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#include "nix/store/dependency-graph-impl.hh"

#include <gtest/gtest.h>

namespace nix {

TEST(DependencyGraph, BasicAddEdge)
{
FilePathGraph depGraph;
depGraph.addEdge("a", "b");
depGraph.addEdge("b", "c");

EXPECT_TRUE(depGraph.hasNode("a"));
EXPECT_TRUE(depGraph.hasNode("b"));
EXPECT_TRUE(depGraph.hasNode("c"));
EXPECT_FALSE(depGraph.hasNode("d"));

// Verify edges using high-level API
auto successors = depGraph.getSuccessors("a");
EXPECT_EQ(successors.size(), 1);
EXPECT_EQ(successors[0], "b");
}

TEST(DependencyGraph, DfsTraversalOrder)
{
// Build a graph: A->B->D, A->C->D
// Successors should be visited in distance order (B and C before recursing)
FilePathGraph depGraph;
depGraph.addEdge("a", "b");
depGraph.addEdge("a", "c");
depGraph.addEdge("b", "d");
depGraph.addEdge("c", "d");

std::vector<std::string> visitedNodes;
std::vector<std::pair<std::string, std::string>> visitedEdges;

depGraph.dfsFromTarget(
"a",
"d",
[&](const std::string & node, size_t depth) {
visitedNodes.push_back(node);
return true;
},
[&](const std::string & from, const std::string & to, bool isLast, size_t depth) {
visitedEdges.emplace_back(from, to);
},
[](const std::string &) { return false; });

EXPECT_EQ(visitedNodes[0], "a");
// B and C both at distance 1, could be in either order
EXPECT_TRUE(
(visitedNodes[1] == "b" && visitedNodes[2] == "d") || (visitedNodes[1] == "c" && visitedNodes[2] == "d"));
}

TEST(DependencyGraph, GetSuccessors)
{
FilePathGraph depGraph;
depGraph.addEdge("a", "b");
depGraph.addEdge("a", "c");

auto successors = depGraph.getSuccessors("a");
EXPECT_EQ(successors.size(), 2);
EXPECT_TRUE(std::ranges::contains(successors, "b"));
EXPECT_TRUE(std::ranges::contains(successors, "c"));
}

TEST(DependencyGraph, GetAllNodes)
{
FilePathGraph depGraph;
depGraph.addEdge("foo", "bar");
depGraph.addEdge("bar", "baz");

auto nodes = depGraph.getAllNodes();
EXPECT_EQ(nodes.size(), 3);
EXPECT_TRUE(std::ranges::contains(nodes, "foo"));
EXPECT_TRUE(std::ranges::contains(nodes, "bar"));
EXPECT_TRUE(std::ranges::contains(nodes, "baz"));
}

TEST(DependencyGraph, ThrowsOnMissingNode)
{
FilePathGraph depGraph;
depGraph.addEdge("a", "b");

EXPECT_THROW(depGraph.getSuccessors("nonexistent"), nix::Error);
}

TEST(DependencyGraph, EmptyGraph)
{
FilePathGraph depGraph;

EXPECT_FALSE(depGraph.hasNode("anything"));
EXPECT_EQ(depGraph.numVertices(), 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
1 change: 1 addition & 0 deletions src/libstore-tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ subdir('nix-meson-build-support/common')
sources = files(
'common-protocol.cc',
'content-address.cc',
'dependency-graph.cc',
'derivation-advanced-attrs.cc',
'derivation.cc',
'derived-path.cc',
Expand Down
18 changes: 9 additions & 9 deletions src/libstore-tests/references.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ TEST_P(RewriteTest, IdentityRewriteIsIdentity)
auto rewriter = RewritingSink(param.rewrites, rewritten);
rewriter(param.originalString);
rewriter.flush();
ASSERT_EQ(rewritten.s, param.finalString);
EXPECT_EQ(rewritten.s, param.finalString);
}

INSTANTIATE_TEST_CASE_P(
Expand All @@ -52,14 +52,14 @@ TEST(references, scan)
RefScanSink scanner(StringSet{hash1});
auto s = "foobar";
scanner(s);
ASSERT_EQ(scanner.getResult(), StringSet{});
EXPECT_EQ(scanner.getResult(), StringSet{});
}

{
RefScanSink scanner(StringSet{hash1});
auto s = "foobar" + hash1 + "xyzzy";
scanner(s);
ASSERT_EQ(scanner.getResult(), StringSet{hash1});
EXPECT_EQ(scanner.getResult(), StringSet{hash1});
}

{
Expand All @@ -69,15 +69,15 @@ TEST(references, scan)
scanner(((std::string_view) s).substr(10, 5));
scanner(((std::string_view) s).substr(15, 5));
scanner(((std::string_view) s).substr(20));
ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
}

{
RefScanSink scanner(StringSet{hash1, hash2});
auto s = "foobar" + hash1 + "xyzzy" + hash2;
for (auto & i : s)
scanner(std::string(1, i));
ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
}
}

Expand Down Expand Up @@ -161,7 +161,7 @@ TEST(references, scanForReferencesDeep)
{
CanonPath f1Path("/file1.txt");
auto it = foundRefs.find(f1Path);
ASSERT_TRUE(it != foundRefs.end());
EXPECT_TRUE(it != foundRefs.end());
EXPECT_EQ(it->second.size(), 1);
EXPECT_TRUE(it->second.count(path1));
}
Expand All @@ -170,7 +170,7 @@ TEST(references, scanForReferencesDeep)
{
CanonPath f2Path("/file2.txt");
auto it = foundRefs.find(f2Path);
ASSERT_TRUE(it != foundRefs.end());
EXPECT_TRUE(it != foundRefs.end());
EXPECT_EQ(it->second.size(), 2);
EXPECT_TRUE(it->second.count(path2));
EXPECT_TRUE(it->second.count(path3));
Expand All @@ -186,7 +186,7 @@ TEST(references, scanForReferencesDeep)
{
CanonPath f4Path("/subdir/file4.txt");
auto it = foundRefs.find(f4Path);
ASSERT_TRUE(it != foundRefs.end());
EXPECT_TRUE(it != foundRefs.end());
EXPECT_EQ(it->second.size(), 1);
EXPECT_TRUE(it->second.count(path1));
}
Expand All @@ -195,7 +195,7 @@ TEST(references, scanForReferencesDeep)
{
CanonPath linkPath("/link1");
auto it = foundRefs.find(linkPath);
ASSERT_TRUE(it != foundRefs.end());
EXPECT_TRUE(it != foundRefs.end());
EXPECT_EQ(it->second.size(), 1);
EXPECT_TRUE(it->second.count(path2));
}
Expand Down
10 changes: 10 additions & 0 deletions src/libstore/dependency-graph.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "nix/store/dependency-graph-impl.hh"

namespace nix {

// Explicit instantiations for common types
template class DependencyGraph<StorePath>;
template class DependencyGraph<std::string>;
template class DependencyGraph<StorePath, FileListEdgeProperty>;

} // namespace nix
Loading
Loading