// Copyright 2017 The Abseil Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "absl/synchronization/internal/graphcycles.h" #include <map> #include <random> #include <unordered_set> #include <utility> #include <vector> #include "gtest/gtest.h" #include "absl/base/internal/raw_logging.h" #include "absl/base/macros.h" namespace absl { ABSL_NAMESPACE_BEGIN namespace synchronization_internal { // We emulate a GraphCycles object with a node vector and an edge vector. // We then compare the two implementations. using Nodes = std::vector<int>; struct Edge { int from; int to; }; using Edges = std::vector<Edge>; using RandomEngine = std::mt19937_64; // Mapping from integer index to GraphId. typedef std::map<int, GraphId> IdMap; static GraphId Get(const IdMap& id, int num) { auto iter = id.find(num); return (iter == id.end()) ? InvalidGraphId() : iter->second; } // Return whether "to" is reachable from "from". static bool IsReachable(Edges *edges, int from, int to, std::unordered_set<int> *seen) { seen->insert(from); // we are investigating "from"; don't do it again if (from == to) return true; for (const auto &edge : *edges) { if (edge.from == from) { if (edge.to == to) { // success via edge directly return true; } else if (seen->find(edge.to) == seen->end() && // success via edge IsReachable(edges, edge.to, to, seen)) { return true; } } } return false; } static void PrintEdges(Edges *edges) { ABSL_RAW_LOG(INFO, "EDGES (%zu)", edges->size()); for (const auto &edge : *edges) { int a = edge.from; int b = edge.to; ABSL_RAW_LOG(INFO, "%d %d", a, b); } ABSL_RAW_LOG(INFO, "---"); } static void PrintGCEdges(Nodes *nodes, const IdMap &id, GraphCycles *gc) { ABSL_RAW_LOG(INFO, "GC EDGES"); for (int a : *nodes) { for (int b : *nodes) { if (gc->HasEdge(Get(id, a), Get(id, b))) { ABSL_RAW_LOG(INFO, "%d %d", a, b); } } } ABSL_RAW_LOG(INFO, "---"); } static void PrintTransitiveClosure(Nodes *nodes, Edges *edges) { ABSL_RAW_LOG(INFO, "Transitive closure"); for (int a : *nodes) { for (int b : *nodes) { std::unordered_set<int> seen; if (IsReachable(edges, a, b, &seen)) { ABSL_RAW_LOG(INFO, "%d %d", a, b); } } } ABSL_RAW_LOG(INFO, "---"); } static void PrintGCTransitiveClosure(Nodes *nodes, const IdMap &id, GraphCycles *gc) { ABSL_RAW_LOG(INFO, "GC Transitive closure"); for (int a : *nodes) { for (int b : *nodes) { if (gc->IsReachable(Get(id, a), Get(id, b))) { ABSL_RAW_LOG(INFO, "%d %d", a, b); } } } ABSL_RAW_LOG(INFO, "---"); } static void CheckTransitiveClosure(Nodes *nodes, Edges *edges, const IdMap &id, GraphCycles *gc) { std::unordered_set<int> seen; for (const auto &a : *nodes) { for (const auto &b : *nodes) { seen.clear(); bool gc_reachable = gc->IsReachable(Get(id, a), Get(id, b)); bool reachable = IsReachable(edges, a, b, &seen); if (gc_reachable != reachable) { PrintEdges(edges); PrintGCEdges(nodes, id, gc); PrintTransitiveClosure(nodes, edges); PrintGCTransitiveClosure(nodes, id, gc); ABSL_RAW_LOG(FATAL, "gc_reachable %s reachable %s a %d b %d", gc_reachable ? "true" : "false", reachable ? "true" : "false", a, b); } } } } static void CheckEdges(Nodes *nodes, Edges *edges, const IdMap &id, GraphCycles *gc) { int count = 0; for (const auto &edge : *edges) { int a = edge.from; int b = edge.to; if (!gc->HasEdge(Get(id, a), Get(id, b))) { PrintEdges(edges); PrintGCEdges(nodes, id, gc); ABSL_RAW_LOG(FATAL, "!gc->HasEdge(%d, %d)", a, b); } } for (const auto &a : *nodes) { for (const auto &b : *nodes) { if (gc->HasEdge(Get(id, a), Get(id, b))) { count++; } } } if (count != edges->size()) { PrintEdges(edges); PrintGCEdges(nodes, id, gc); ABSL_RAW_LOG(FATAL, "edges->size() %zu count %d", edges->size(), count); } } static void CheckInvariants(const GraphCycles &gc) { if (ABSL_PREDICT_FALSE(!gc.CheckInvariants())) ABSL_RAW_LOG(FATAL, "CheckInvariants"); } // Returns the index of a randomly chosen node in *nodes. // Requires *nodes be non-empty. static int RandomNode(RandomEngine* rng, Nodes *nodes) { std::uniform_int_distribution<int> uniform(0, nodes->size()-1); return uniform(*rng); } // Returns the index of a randomly chosen edge in *edges. // Requires *edges be non-empty. static int RandomEdge(RandomEngine* rng, Edges *edges) { std::uniform_int_distribution<int> uniform(0, edges->size()-1); return uniform(*rng); } // Returns the index of edge (from, to) in *edges or -1 if it is not in *edges. static int EdgeIndex(Edges *edges, int from, int to) { int i = 0; while (i != edges->size() && ((*edges)[i].from != from || (*edges)[i].to != to)) { i++; } return i == edges->size()? -1 : i; } TEST(GraphCycles, RandomizedTest) { int next_node = 0; Nodes nodes; Edges edges; // from, to IdMap id; GraphCycles graph_cycles; static const int kMaxNodes = 7; // use <= 7 nodes to keep test short static const int kDataOffset = 17; // an offset to the node-specific data int n = 100000; int op = 0; RandomEngine rng(testing::UnitTest::GetInstance()->random_seed()); std::uniform_int_distribution<int> uniform(0, 5); auto ptr = [](intptr_t i) { return reinterpret_cast<void*>(i + kDataOffset); }; for (int iter = 0; iter != n; iter++) { for (const auto &node : nodes) { ASSERT_EQ(graph_cycles.Ptr(Get(id, node)), ptr(node)) << " node " << node; } CheckEdges(&nodes, &edges, id, &graph_cycles); CheckTransitiveClosure(&nodes, &edges, id, &graph_cycles); op = uniform(rng); switch (op) { case 0: // Add a node if (nodes.size() < kMaxNodes) { int new_node = next_node++; GraphId new_gnode = graph_cycles.GetId(ptr(new_node)); ASSERT_NE(new_gnode, InvalidGraphId()); id[new_node] = new_gnode; ASSERT_EQ(ptr(new_node), graph_cycles.Ptr(new_gnode)); nodes.push_back(new_node); } break; case 1: // Remove a node if (nodes.size() > 0) { int node_index = RandomNode(&rng, &nodes); int node = nodes[node_index]; nodes[node_index] = nodes.back(); nodes.pop_back(); graph_cycles.RemoveNode(ptr(node)); ASSERT_EQ(graph_cycles.Ptr(Get(id, node)), nullptr); id.erase(node); int i = 0; while (i != edges.size()) { if (edges[i].from == node || edges[i].to == node) { edges[i] = edges.back(); edges.pop_back(); } else { i++; } } } break; case 2: // Add an edge if (nodes.size() > 0) { int from = RandomNode(&rng, &nodes); int to = RandomNode(&rng, &nodes); if (EdgeIndex(&edges, nodes[from], nodes[to]) == -1) { if (graph_cycles.InsertEdge(id[nodes[from]], id[nodes[to]])) { Edge new_edge; new_edge.from = nodes[from]; new_edge.to = nodes[to]; edges.push_back(new_edge); } else { std::unordered_set<int> seen; ASSERT_TRUE(IsReachable(&edges, nodes[to], nodes[from], &seen)) << "Edge " << nodes[to] << "->" << nodes[from]; } } } break; case 3: // Remove an edge if (edges.size() > 0) { int i = RandomEdge(&rng, &edges); int from = edges[i].from; int to = edges[i].to; ASSERT_EQ(i, EdgeIndex(&edges, from, to)); edges[i] = edges.back(); edges.pop_back(); ASSERT_EQ(-1, EdgeIndex(&edges, from, to)); graph_cycles.RemoveEdge(id[from], id[to]); } break; case 4: // Check a path if (nodes.size() > 0) { int from = RandomNode(&rng, &nodes); int to = RandomNode(&rng, &nodes); GraphId path[2*kMaxNodes]; int path_len = graph_cycles.FindPath(id[nodes[from]], id[nodes[to]], ABSL_ARRAYSIZE(path), path); std::unordered_set<int> seen; bool reachable = IsReachable(&edges, nodes[from], nodes[to], &seen); bool gc_reachable = graph_cycles.IsReachable(Get(id, nodes[from]), Get(id, nodes[to])); ASSERT_EQ(path_len != 0, reachable); ASSERT_EQ(path_len != 0, gc_reachable); // In the following line, we add one because a node can appear // twice, if the path is from that node to itself, perhaps via // every other node. ASSERT_LE(path_len, kMaxNodes + 1); if (path_len != 0) { ASSERT_EQ(id[nodes[from]], path[0]); ASSERT_EQ(id[nodes[to]], path[path_len-1]); for (int i = 1; i < path_len; i++) { ASSERT_TRUE(graph_cycles.HasEdge(path[i-1], path[i])); } } } break; case 5: // Check invariants CheckInvariants(graph_cycles); break; default: ABSL_RAW_LOG(FATAL, "op %d", op); } // Very rarely, test graph expansion by adding then removing many nodes. std::bernoulli_distribution one_in_1024(1.0 / 1024); if (one_in_1024(rng)) { CheckEdges(&nodes, &edges, id, &graph_cycles); CheckTransitiveClosure(&nodes, &edges, id, &graph_cycles); for (int i = 0; i != 256; i++) { int new_node = next_node++; GraphId new_gnode = graph_cycles.GetId(ptr(new_node)); ASSERT_NE(InvalidGraphId(), new_gnode); id[new_node] = new_gnode; ASSERT_EQ(ptr(new_node), graph_cycles.Ptr(new_gnode)); for (const auto &node : nodes) { ASSERT_NE(node, new_node); } nodes.push_back(new_node); } for (int i = 0; i != 256; i++) { ASSERT_GT(nodes.size(), 0); int node_index = RandomNode(&rng, &nodes); int node = nodes[node_index]; nodes[node_index] = nodes.back(); nodes.pop_back(); graph_cycles.RemoveNode(ptr(node)); id.erase(node); int j = 0; while (j != edges.size()) { if (edges[j].from == node || edges[j].to == node) { edges[j] = edges.back(); edges.pop_back(); } else { j++; } } } CheckInvariants(graph_cycles); } } } class GraphCyclesTest : public ::testing::Test { public: IdMap id_; GraphCycles g_; static void* Ptr(int i) { return reinterpret_cast<void*>(static_cast<uintptr_t>(i)); } static int Num(void* ptr) { return static_cast<int>(reinterpret_cast<uintptr_t>(ptr)); } // Test relies on ith NewNode() call returning Node numbered i GraphCyclesTest() { for (int i = 0; i < 100; i++) { id_[i] = g_.GetId(Ptr(i)); } CheckInvariants(g_); } bool AddEdge(int x, int y) { return g_.InsertEdge(Get(id_, x), Get(id_, y)); } void AddMultiples() { // For every node x > 0: add edge to 2*x, 3*x for (int x = 1; x < 25; x++) { EXPECT_TRUE(AddEdge(x, 2*x)) << x; EXPECT_TRUE(AddEdge(x, 3*x)) << x; } CheckInvariants(g_); } std::string Path(int x, int y) { GraphId path[5]; int np = g_.FindPath(Get(id_, x), Get(id_, y), ABSL_ARRAYSIZE(path), path); std::string result; for (int i = 0; i < np; i++) { if (i >= ABSL_ARRAYSIZE(path)) { result += " ..."; break; } if (!result.empty()) result.push_back(' '); char buf[20]; snprintf(buf, sizeof(buf), "%d", Num(g_.Ptr(path[i]))); result += buf; } return result; } }; TEST_F(GraphCyclesTest, NoCycle) { AddMultiples(); CheckInvariants(g_); } TEST_F(GraphCyclesTest, SimpleCycle) { AddMultiples(); EXPECT_FALSE(AddEdge(8, 4)); EXPECT_EQ("4 8", Path(4, 8)); CheckInvariants(g_); } TEST_F(GraphCyclesTest, IndirectCycle) { AddMultiples(); EXPECT_TRUE(AddEdge(16, 9)); CheckInvariants(g_); EXPECT_FALSE(AddEdge(9, 2)); EXPECT_EQ("2 4 8 16 9", Path(2, 9)); CheckInvariants(g_); } TEST_F(GraphCyclesTest, LongPath) { ASSERT_TRUE(AddEdge(2, 4)); ASSERT_TRUE(AddEdge(4, 6)); ASSERT_TRUE(AddEdge(6, 8)); ASSERT_TRUE(AddEdge(8, 10)); ASSERT_TRUE(AddEdge(10, 12)); ASSERT_FALSE(AddEdge(12, 2)); EXPECT_EQ("2 4 6 8 10 ...", Path(2, 12)); CheckInvariants(g_); } TEST_F(GraphCyclesTest, RemoveNode) { ASSERT_TRUE(AddEdge(1, 2)); ASSERT_TRUE(AddEdge(2, 3)); ASSERT_TRUE(AddEdge(3, 4)); ASSERT_TRUE(AddEdge(4, 5)); g_.RemoveNode(g_.Ptr(id_[3])); id_.erase(3); ASSERT_TRUE(AddEdge(5, 1)); } TEST_F(GraphCyclesTest, ManyEdges) { const int N = 50; for (int i = 0; i < N; i++) { for (int j = 1; j < N; j++) { ASSERT_TRUE(AddEdge(i, i+j)); } } CheckInvariants(g_); ASSERT_TRUE(AddEdge(2*N-1, 0)); CheckInvariants(g_); ASSERT_FALSE(AddEdge(10, 9)); CheckInvariants(g_); } } // namespace synchronization_internal ABSL_NAMESPACE_END } // namespace absl