|  | // 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 |