From 2030368c22082f7dba13703fcdec57bdd974b749 Mon Sep 17 00:00:00 2001 From: Ian Roddis Date: Mon, 7 Jun 2021 11:54:50 -0300 Subject: [PATCH] Adding DAG visitor and some unit tests around its function --- daggy/include/daggy/DAG.hpp | 26 +++------ daggy/include/daggy/{DAG.impl => DAGImpl.hpp} | 58 +++++++++++-------- daggy/include/daggy/DAGVisitor.hpp | 36 ++++++++++++ daggy/include/daggy/DAGVisitorImpl.hpp | 42 ++++++++++++++ tests/unit_dag.cpp | 31 ++++++++-- tests/unit_dag_visitor.cpp | 34 +++++++++++ 6 files changed, 181 insertions(+), 46 deletions(-) rename daggy/include/daggy/{DAG.impl => DAGImpl.hpp} (51%) create mode 100644 daggy/include/daggy/DAGVisitor.hpp create mode 100644 daggy/include/daggy/DAGVisitorImpl.hpp create mode 100644 tests/unit_dag_visitor.cpp diff --git a/daggy/include/daggy/DAG.hpp b/daggy/include/daggy/DAG.hpp index b46baf6..d75817a 100644 --- a/daggy/include/daggy/DAG.hpp +++ b/daggy/include/daggy/DAG.hpp @@ -23,8 +23,8 @@ namespace daggy { // Vertices void addVertex(T id); void dropVertex(const T & id); - void setVertexVisited(const T & id, bool visited); - void setDAGVisited(bool visited); + const std::unordered_set & getChildren(const T & id) const; + const std::unordered_set & getRoots() const; // Edges void addEdge(const T & src, const T & dst); @@ -33,24 +33,14 @@ namespace daggy { // Returns the path from {from} to {to} std::deque shortestPath(const T & from, const T & to); - // Traversal - void initTraversal(std::vector ids = {}); - const T & visitNextVertex(); + // Attributes + size_t size() const; + bool empty() const; private: - struct Vertex { - // It's a bit redundant to preserve both, but - // it makes it possible with and against the - // directions possible - std::unordered_set parents; - std::unordered_set children; - bool visited; - }; - - std::unordered_map vertices; - std::unordered_set roots; - std::unordered_set active_nodes; + std::unordered_map> vertices_; + std::unordered_set roots_; }; -#include "DAG.impl" +#include "DAGImpl.hpp" } diff --git a/daggy/include/daggy/DAG.impl b/daggy/include/daggy/DAGImpl.hpp similarity index 51% rename from daggy/include/daggy/DAG.impl rename to daggy/include/daggy/DAGImpl.hpp index b56847c..5805e21 100644 --- a/daggy/include/daggy/DAG.impl +++ b/daggy/include/daggy/DAGImpl.hpp @@ -1,45 +1,45 @@ +template +size_t DAG::size() const { + return vertices_.size(); +} + +template +bool DAG::empty() const { + return vertices_.empty(); +} + template void DAG::addVertex(T id) { - if (vertices.find(id) != vertices.end()) + if (vertices_.find(id) != vertices_.end()) throw std::runtime_error("Vertex already exists in graph"); - vertices.emplace(id, Vertex{.visited = false}); - roots.insert(id); + vertices_[id]; + roots_.insert(id); } template void DAG::dropVertex(const T & id) { - auto it = vertices.find(id); - auto vert = vertices.extract(id); - for (auto cid : vert.children) dropEdge(id, cid); - for (auto pid : vert.parents) dropEdge(pid, id); - - // Remove anything - roots.extract(id); + vertices_.extract(id); + roots_.extract(id); } template void DAG::dropEdge(const T & from, const T & to) { - auto & src = vertices.at(from); - auto & dst = vertices.at(to); - - src.children.extract(to); - dst.parents.extract(from); - - roots.extract(to); + auto & src = vertices_.at(from); + src.extract(to); + roots_.extract(to); } template void DAG::addEdge(const T & from, const T & to) { - auto & src = vertices.at(from); - auto & dst = vertices.at(to); + auto & src = vertices_.at(from); if (shortestPath(to, from).size() > 1) { throw std::runtime_error("Unable to add edge that would result in a cycle"); } // Add the edge - src.children.insert(to); - dst.parents.insert(from); + src.insert(to); + roots_.extract(to); } template @@ -48,16 +48,28 @@ std::deque DAG::shortestPath(const T & from, const T & to) { if (from == to) return {to}; - auto & src = vertices.at(from); + auto & src = vertices_.at(from); - for (const auto & cid : src.children) { + for (const auto & cid : src) { auto pth = shortestPath(cid, to); if (subpath.size() == 0 or subpath.size() > pth.size()) subpath.swap(pth); } + if (subpath.size() == 0) return subpath; + subpath.push_front(from); return subpath; } +template +const std::unordered_set & DAG::getChildren(const T & id) const { + return vertices_.at(id); +} + +template +const std::unordered_set & DAG::getRoots() const { + return roots_; +} + diff --git a/daggy/include/daggy/DAGVisitor.hpp b/daggy/include/daggy/DAGVisitor.hpp new file mode 100644 index 0000000..2eae226 --- /dev/null +++ b/daggy/include/daggy/DAGVisitor.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "DAG.hpp" + +/* + The DAG structure in daggy is just to ensure that tasks are run + in the correct dependent order. +*/ + +namespace daggy { + template + class DAGVisitor { + public: + DAGVisitor(const DAG & dag, std::unordered_set roots = {}); + + std::optional visitNext(); // Get the next ID + void completeVisit(const T& id); // Mark the ID + bool isComplete() const; // True if the graph has been fully traversed + + private: + const DAG & dag_; + std::deque queued_; + std::unordered_set processing_; + std::unordered_set complete_; + }; + +#include "DAGVisitorImpl.hpp" +} diff --git a/daggy/include/daggy/DAGVisitorImpl.hpp b/daggy/include/daggy/DAGVisitorImpl.hpp new file mode 100644 index 0000000..548d74e --- /dev/null +++ b/daggy/include/daggy/DAGVisitorImpl.hpp @@ -0,0 +1,42 @@ +template +DAGVisitor::DAGVisitor(const DAG & dag, std::unordered_set roots) + : dag_(dag) { + if (roots.size() == 0) { + for (const auto & id : dag_.getRoots()) { + queued_.emplace_back(id); + } + } else { + for (auto & id : roots) { + queued_.push_back(id); + } + } +} + +template +std::optional DAGVisitor::visitNext() { + if (queued_.empty()) return {}; + const auto & id = queued_.front(); + processing_.insert(id); + queued_.pop_front(); + return id; +}; // Get the next ID + +template +void DAGVisitor::completeVisit(const T& id) { + auto entry = processing_.extract(id); + complete_.insert(entry.value()); + for (const auto & c : dag_.getChildren(id)) { + if (complete_.find(c) == complete_.end()) { + queued_.push_back(c); + } + } +} + +template +bool DAGVisitor::isComplete() const { + bool complete = queued_.empty(); + for (const auto & p : processing_) { + complete &= dag_.getChildren(p).empty(); + } + return complete; +}; diff --git a/tests/unit_dag.cpp b/tests/unit_dag.cpp index c145ef6..761c9c9 100644 --- a/tests/unit_dag.cpp +++ b/tests/unit_dag.cpp @@ -1,9 +1,30 @@ #include #include "daggy/DAG.hpp" +#include "daggy/DAGVisitor.hpp" #include "catch.hpp" +TEST_CASE("DAG Construction Tests", "[dag]") { + daggy::DAG dag; + + REQUIRE(dag.size() == 0); + REQUIRE(dag.empty()); + + REQUIRE_NOTHROW(dag.addVertex(0)); + for (int i = 1; i < 10; ++i) { + dag.addVertex(i); + dag.addEdge(i-1, i); + } + + REQUIRE(dag.getRoots().size() == 1); + REQUIRE(dag.size() == 10); + REQUIRE(! dag.empty()); + + // Cannot add an edge that would result in a cycle + REQUIRE_THROWS(dag.addEdge(9, 5)); +} + TEST_CASE("DAG Basic Tests", "[dag]") { daggy::DAG dag; @@ -13,10 +34,10 @@ TEST_CASE("DAG Basic Tests", "[dag]") { dag.addEdge(i-1, i); } - REQUIRE(dag.shortestPath(0,9).size() == 10); + SECTION("Pathing") { + REQUIRE(dag.shortestPath(0,9).size() == 10); - dag.addEdge(5, 9); - REQUIRE(dag.shortestPath(0,9).size() == 7); - - REQUIRE_THROWS(dag.addEdge(9, 5)); + dag.addEdge(5, 9); + REQUIRE(dag.shortestPath(0,9).size() == 7); + } } diff --git a/tests/unit_dag_visitor.cpp b/tests/unit_dag_visitor.cpp new file mode 100644 index 0000000..c8422f4 --- /dev/null +++ b/tests/unit_dag_visitor.cpp @@ -0,0 +1,34 @@ +#include + +#include "daggy/DAGVisitor.hpp" + +#include "catch.hpp" + +TEST_CASE("DAG Visitor Tests", "[dagvisitor]") { + daggy::DAG dag; + + REQUIRE_NOTHROW(dag.addVertex(0)); + for (int i = 1; i < 10; ++i) { + dag.addVertex(i); + dag.addEdge(i-1, i); + } + + // Add an edge to make it interesting + dag.addEdge(5, 7); + + // for (auto v : dag.getRoots()) std::cout << " " << v << std::endl; + + SECTION("Basic traversal") { + daggy::DAGVisitor visitor(dag); + REQUIRE(! visitor.isComplete()); + + size_t i = 0; + while (! visitor.isComplete()) { + auto id = visitor.visitNext(); + REQUIRE(id.has_value()); + visitor.completeVisit(id.value()); + ++i; + } + REQUIRE(i == 10); + } +}