diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 52f57a4dcc..369220ab6a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -171,6 +171,7 @@ set(ARANGODB_TESTS_SOURCES Geo/GeoFunctionsTest.cpp Geo/NearUtilsTest.cpp Geo/ShapeContainerTest.cpp + Graph/GraphTestTools.cpp Graph/ClusterTraverserCacheTest.cpp Graph/ConstantWeightShortestPathFinder.cpp Graph/KShortestPathsFinder.cpp diff --git a/tests/Graph/ConstantWeightShortestPathFinder.cpp b/tests/Graph/ConstantWeightShortestPathFinder.cpp index abf825aaec..d2363a277f 100644 --- a/tests/Graph/ConstantWeightShortestPathFinder.cpp +++ b/tests/Graph/ConstantWeightShortestPathFinder.cpp @@ -54,243 +54,13 @@ #include "../Mocks/StorageEngineMock.h" #include "IResearch/common.h" +#include "GraphTestTools.h" + using namespace arangodb; using namespace arangodb::aql; using namespace arangodb::graph; using namespace arangodb::velocypack; -// std::unordered_set unused; -// auto builder = edges->toVelocyPackIgnore(unused, false, false); -// LOG_DEVEL << builder.toJson(); - -namespace { -struct Setup { - StorageEngineMock engine; - arangodb::application_features::ApplicationServer server; - std::unique_ptr system; - std::vector> features; - - Setup() : engine(server), server(nullptr, nullptr) { - arangodb::EngineSelectorFeature::ENGINE = &engine; - arangodb::transaction::Methods::clearDataSourceRegistrationCallbacks(); - arangodb::ClusterEngine::Mocking = true; - arangodb::RandomGenerator::initialize(arangodb::RandomGenerator::RandomType::MERSENNE); - - // suppress log messages since tests check error conditions - arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), arangodb::LogLevel::ERR); // suppress WARNING DefaultCustomTypeHandler called - - // setup required application features - features.emplace_back(new arangodb::DatabasePathFeature(server), false); - features.emplace_back(new arangodb::DatabaseFeature(server), false); - features.emplace_back(new arangodb::QueryRegistryFeature(server), false); // must be first - arangodb::application_features::ApplicationServer::server->addFeature( - features.back().first); // need QueryRegistryFeature feature to be added now in order to create the system database - system = std::make_unique(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, - 0, TRI_VOC_SYSTEM_DATABASE); - features.emplace_back(new arangodb::SystemDatabaseFeature(server, system.get()), - false); // required for IResearchAnalyzerFeature - features.emplace_back(new arangodb::TraverserEngineRegistryFeature(server), false); // must be before AqlFeature - features.emplace_back(new arangodb::AqlFeature(server), true); - features.emplace_back(new arangodb::aql::OptimizerRulesFeature(server), true); - features.emplace_back(new arangodb::aql::AqlFunctionFeature(server), true); // required for IResearchAnalyzerFeature - - for (auto& f : features) { - arangodb::application_features::ApplicationServer::server->addFeature(f.first); - } - - for (auto& f : features) { - f.first->prepare(); - } - - for (auto& f : features) { - if (f.second) { - f.first->start(); - } - } - - auto* dbPathFeature = - arangodb::application_features::ApplicationServer::getFeature( - "DatabasePath"); - arangodb::tests::setDatabasePath(*dbPathFeature); // ensure test data is stored in a unique directory - } - - ~Setup() { - system.reset(); // destroy before reseting the 'ENGINE' - arangodb::AqlFeature(server).stop(); // unset singleton instance - arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), - arangodb::LogLevel::DEFAULT); - arangodb::application_features::ApplicationServer::server = nullptr; - arangodb::EngineSelectorFeature::ENGINE = nullptr; - - // destroy application features - for (auto& f : features) { - if (f.second) { - f.first->stop(); - } - } - - for (auto& f : features) { - f.first->unprepare(); - } - } -}; // Setup - -struct MockGraphDatabase { - TRI_vocbase_t vocbase; - std::vector queries; - std::vector spos; - - MockGraphDatabase(std::string name) - : vocbase(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, 1, name) {} - - ~MockGraphDatabase() { - for (auto& q : queries) { - if (q->trx() != nullptr) { - q->trx()->abort(); - } - delete q; - } - for (auto& o : spos) { - delete o; - } - } - - // Create a collection with name and vertices - // with ids 0..n-1 - void addVertexCollection(std::string name, size_t n) { - std::shared_ptr vertices; - - auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + - "\", \"type\": 2 }"); - vertices = vocbase.createCollection(createJson->slice()); - REQUIRE((nullptr != vertices)); - - std::vector insertedDocs; - std::vector> docs; - for (size_t i = 0; i < n; i++) { - docs.emplace_back(arangodb::velocypack::Parser::fromJson( - "{ \"_key\": \"" + std::to_string(i) + "\"}")); - }; - - arangodb::OperationOptions options; - options.returnNew = true; - arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), - *vertices, arangodb::AccessMode::Type::WRITE); - CHECK((trx.begin().ok())); - - for (auto& entry : docs) { - auto res = trx.insert(vertices->name(), entry->slice(), options); - CHECK((res.ok())); - insertedDocs.emplace_back(res.slice().get("new")); - } - - CHECK((trx.commit().ok())); - CHECK(insertedDocs.size() == n); - CHECK(vertices->type()); - } - - // Create a collection with name of edges given by - void addEdgeCollection(std::string name, std::string vertexCollection, - std::vector> edgedef) { - std::shared_ptr edges; - auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + - "\", \"type\": 3 }"); - edges = vocbase.createCollection(createJson->slice()); - REQUIRE((nullptr != edges)); - - auto indexJson = velocypack::Parser::fromJson("{ \"type\": \"edge\" }"); - bool created = false; - auto index = edges->createIndex(indexJson->slice(), created); - CHECK(index); - CHECK(created); - - std::vector insertedDocs; - std::vector> docs; - - for (auto& p : edgedef) { - // std::cout << "edge: " << vertexCollection << " " << p.first << " -> " - // << p.second << std::endl; - auto docJson = velocypack::Parser::fromJson( - "{ \"_from\": \"" + vertexCollection + "/" + std::to_string(p.first) + - "\"" + ", \"_to\": \"" + vertexCollection + "/" + - std::to_string(p.second) + "\" }"); - docs.emplace_back(docJson); - } - - arangodb::OperationOptions options; - options.returnNew = true; - arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), - *edges, arangodb::AccessMode::Type::WRITE); - CHECK((trx.begin().ok())); - - for (auto& entry : docs) { - auto res = trx.insert(edges->name(), entry->slice(), options); - CHECK((res.ok())); - insertedDocs.emplace_back(res.slice().get("new")); - } - - CHECK((trx.commit().ok())); - CHECK(insertedDocs.size() == edgedef.size()); - } - - arangodb::aql::Query* getQuery(std::string qry) { - auto queryString = arangodb::aql::QueryString(qry); - - arangodb::aql::Query* query = - new arangodb::aql::Query(false, vocbase, queryString, nullptr, - arangodb::velocypack::Parser::fromJson("{}"), - arangodb::aql::PART_MAIN); - query->parse(); - query->prepare(arangodb::QueryRegistryFeature::registry()); - - queries.emplace_back(query); - - return query; - } - - arangodb::graph::ShortestPathOptions* getShortestPathOptions(arangodb::aql::Query* query) { - arangodb::graph::ShortestPathOptions* spo; - - auto plan = query->plan(); - auto ast = plan->getAst(); - - auto _toCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); - auto _fromCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); - - auto tmpVar = plan->getAst()->variables()->createTemporaryVariable(); - - AstNode* tmpId1 = plan->getAst()->createNodeReference(tmpVar); - AstNode* tmpId2 = plan->getAst()->createNodeValueString("", 0); - - { - auto const* access = - ast->createNodeAttributeAccess(tmpId1, StaticStrings::ToString.c_str(), - StaticStrings::ToString.length()); - auto const* cond = - ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); - _toCondition->addMember(cond); - } - - { - auto const* access = - ast->createNodeAttributeAccess(tmpId1, StaticStrings::FromString.c_str(), - StaticStrings::FromString.length()); - auto const* cond = - ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); - _fromCondition->addMember(cond); - } - spo = new ShortestPathOptions(query); - spo->setVariable(tmpVar); - spo->addLookupInfo(plan, "e", StaticStrings::FromString, _fromCondition->clone(ast)); - spo->addReverseLookupInfo(plan, "e", StaticStrings::ToString, _toCondition->clone(ast)); - - spos.emplace_back(spo); - return spo; - } -}; - -} // namespace - namespace arangodb { namespace tests { namespace graph { @@ -310,39 +80,6 @@ TEST_CASE("ConstantWeightShortestPathFinder", "[graph]") { auto query = gdb.getQuery("RETURN 1"); auto spo = gdb.getShortestPathOptions(query); - auto checkPath = [spo](ShortestPathResult result, std::vector vertices, - std::vector> edges) -> bool { - bool res = true; - - if (result.length() != vertices.size()) return false; - - for (size_t i = 0; i < result.length(); i++) { - AqlValue vert = result.vertexToAqlValue(spo->cache(), i); - AqlValueGuard guard{vert, true}; - if (!vert.slice().get(StaticStrings::KeyString).isEqualString(vertices.at(i))) { - LOG_DEVEL << "expected vertex " << vertices.at(i) << " but found " - << vert.slice().get(StaticStrings::KeyString).toString(); - res = false; - } - } - - // First edge is by convention null - CHECK((result.edgeToAqlValue(spo->cache(), 0).isNull(true))); - for (size_t i = 1; i < result.length(); i++) { - AqlValue edge = result.edgeToAqlValue(spo->cache(), i); - AqlValueGuard guard{edge, true}; - if (!edge.slice().get(StaticStrings::FromString).isEqualString(edges.at(i).first) || - !edge.slice().get(StaticStrings::ToString).isEqualString(edges.at(i).second)) { - LOG_DEVEL << "expected edge " << edges.at(i).first << " -> " - << edges.at(i).second << " but found " - << edge.slice().get(StaticStrings::FromString).toString() << " -> " - << edge.slice().get(StaticStrings::ToString).toString(); - res = false; - } - } - return res; - }; - auto finder = new ConstantWeightShortestPathFinder(*spo); SECTION("path from vertex to itself") { @@ -366,28 +103,32 @@ TEST_CASE("ConstantWeightShortestPathFinder", "[graph]") { auto start = velocypack::Parser::fromJson("\"v/1\""); auto end = velocypack::Parser::fromJson("\"v/2\""); ShortestPathResult result; + std::string msgs; auto rr = finder->shortestPath(start->slice(), end->slice(), result); REQUIRE(rr); - REQUIRE(checkPath(result, {"1", "2"}, {{}, {"v/1", "v/2"}})); + auto cpr = checkPath(spo, result, {"1", "2"}, {{}, {"v/1", "v/2"}}, msgs); + REQUIRE_MESSAGE(cpr, msgs); } SECTION("path of length 4") { auto start = velocypack::Parser::fromJson("\"v/1\""); auto end = velocypack::Parser::fromJson("\"v/4\""); ShortestPathResult result; + std::string msgs; auto rr = finder->shortestPath(start->slice(), end->slice(), result); REQUIRE(rr); - REQUIRE(checkPath(result, {"1", "2", "3", "4"}, - {{}, {"v/1", "v/2"}, {"v/2", "v/3"}, {"v/3", "v/4"}})); + auto cpr = checkPath(spo, result, {"1", "2", "3", "4"}, + {{}, {"v/1", "v/2"}, {"v/2", "v/3"}, {"v/3", "v/4"}}, msgs); + REQUIRE_MESSAGE(cpr, msgs); } SECTION("two paths of length 5") { auto start = velocypack::Parser::fromJson("\"v/21\""); auto end = velocypack::Parser::fromJson("\"v/25\""); - ShortestPathResult result; + std::string msgs; { auto rr = finder->shortestPath(start->slice(), end->slice(), result); @@ -395,23 +136,25 @@ TEST_CASE("ConstantWeightShortestPathFinder", "[graph]") { REQUIRE(rr); // One of the two has to be returned // This of course leads to output from the LOG_DEVEL in checkPath - CHECK((checkPath(result, {"21", "22", "23", "24", "25"}, - {{}, - {"v/21", "v/22"}, - {"v/22", "v/23"}, - {"v/23", "v/24"}, - {"v/24", "v/25"}}) || - checkPath(result, {"21", "26", "27", "28", "25"}, - {{}, - {"v/21", "v/26"}, - {"v/26", "v/27"}, - {"v/27", "v/28"}, - {"v/28", "v/25"}}))); + auto cpr = checkPath(spo, result, {"21", "22", "23", "24", "25"}, + {{}, + {"v/21", "v/22"}, + {"v/22", "v/23"}, + {"v/23", "v/24"}, + {"v/24", "v/25"}}, + msgs) || + checkPath(spo, result, {"21", "26", "27", "28", "25"}, + {{}, + {"v/21", "v/26"}, + {"v/26", "v/27"}, + {"v/27", "v/28"}, + {"v/28", "v/25"}}, + msgs); + REQUIRE_MESSAGE(cpr, msgs); } { auto rr = finder->shortestPath(end->slice(), start->slice(), result); - REQUIRE(!rr); } } diff --git a/tests/Graph/GraphTestTools.cpp b/tests/Graph/GraphTestTools.cpp new file mode 100644 index 0000000000..33d79a98cf --- /dev/null +++ b/tests/Graph/GraphTestTools.cpp @@ -0,0 +1,51 @@ +// test setup +#include "../Mocks/Servers.h" +#include "../Mocks/StorageEngineMock.h" +#include "IResearch/common.h" +#include "GraphTestTools.h" + +using namespace arangodb; +using namespace arangodb::aql; +using namespace arangodb::graph; +using namespace arangodb::velocypack; + +namespace arangodb { +namespace tests { +namespace graph { + +bool checkPath(ShortestPathOptions* spo, ShortestPathResult result, std::vector vertices, + std::vector> edges, std::string& msgs) { + bool res = true; + + if (result.length() != vertices.size()) return false; + + for (size_t i = 0; i < result.length(); i++) { + AqlValue vert = result.vertexToAqlValue(spo->cache(), i); + AqlValueGuard guard{vert, true}; + if (!vert.slice().get(StaticStrings::KeyString).isEqualString(vertices.at(i))) { + msgs += "expected vertex " + vertices.at(i) + " but found " + + vert.slice().get(StaticStrings::KeyString).toString() + "\n"; + res = false; + } + } + + // First edge is by convention null + CHECK((result.edgeToAqlValue(spo->cache(), 0).isNull(true))); + for (size_t i = 1; i < result.length(); i++) { + AqlValue edge = result.edgeToAqlValue(spo->cache(), i); + AqlValueGuard guard{edge, true}; + if (!edge.slice().get(StaticStrings::FromString).isEqualString(edges.at(i).first) || + !edge.slice().get(StaticStrings::ToString).isEqualString(edges.at(i).second)) { + msgs += "expected edge " + edges.at(i).first + " -> " + + edges.at(i).second + " but found " + + edge.slice().get(StaticStrings::FromString).toString() + " -> " + + edge.slice().get(StaticStrings::ToString).toString() + "\n"; + res = false; + } + } + return res; +} + +} +} +} diff --git a/tests/Graph/GraphTestTools.h b/tests/Graph/GraphTestTools.h new file mode 100644 index 0000000000..c257fc1dd0 --- /dev/null +++ b/tests/Graph/GraphTestTools.h @@ -0,0 +1,305 @@ +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2019 ArangoDB GmbH, Cologne, Germany +/// +/// 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 +/// +/// http://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. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Markus Pfeiffer +//////////////////////////////////////////////////////////////////////////////// +#ifndef TESTS_GRAPH_TOOLS_H +#define TESTS_GRAPH_TOOLS_H + +#include "catch.hpp" + +#include "Aql/AqlFunctionFeature.h" +#include "Aql/Ast.h" +#include "Aql/ExecutionPlan.h" +#include "Aql/OptimizerRulesFeature.h" +#include "Aql/Query.h" +#include "ClusterEngine/ClusterEngine.h" +#include "Graph/ConstantWeightShortestPathFinder.h" +#include "Graph/ShortestPathOptions.h" +#include "Graph/ShortestPathResult.h" +#include "Random/RandomGenerator.h" +#include "RestServer/AqlFeature.h" +#include "RestServer/DatabaseFeature.h" +#include "RestServer/DatabasePathFeature.h" +#include "RestServer/QueryRegistryFeature.h" +#include "RestServer/SystemDatabaseFeature.h" +#include "RestServer/TraverserEngineRegistryFeature.h" +#include "StorageEngine/EngineSelectorFeature.h" +#include "Transaction/Methods.h" +#include "Transaction/StandaloneContext.h" +#include "Utils/SingleCollectionTransaction.h" +#include "VocBase/LogicalCollection.h" + +#include "../Mocks/Servers.h" +#include "../Mocks/StorageEngineMock.h" +#include "IResearch/common.h" + +// Custom failure messages for CHECK and REQUIRE +#define CHECK_MESSAGE(cond, msg) \ + do { \ + INFO(msg); \ + CHECK(cond); \ + } while ((void)0, 0) +#define REQUIRE_MESSAGE(cond, msg) \ + do { \ + INFO(msg); \ + REQUIRE(cond); \ + } while ((void)0, 0) + +using namespace arangodb; +using namespace arangodb::aql; +using namespace arangodb::graph; +using namespace arangodb::velocypack; + +namespace arangodb { +namespace tests { +namespace graph { + +struct Setup { + StorageEngineMock engine; + arangodb::application_features::ApplicationServer server; + std::unique_ptr system; + std::vector> features; + + Setup() : engine(server), server(nullptr, nullptr) { + arangodb::EngineSelectorFeature::ENGINE = &engine; + arangodb::transaction::Methods::clearDataSourceRegistrationCallbacks(); + arangodb::ClusterEngine::Mocking = true; + arangodb::RandomGenerator::initialize(arangodb::RandomGenerator::RandomType::MERSENNE); + + // suppress log messages since tests check error conditions + arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), arangodb::LogLevel::ERR); // suppress WARNING DefaultCustomTypeHandler called + + // setup required application features + features.emplace_back(new arangodb::DatabasePathFeature(server), false); + features.emplace_back(new arangodb::DatabaseFeature(server), false); + features.emplace_back(new arangodb::QueryRegistryFeature(server), false); // must be first + arangodb::application_features::ApplicationServer::server->addFeature( + features.back().first); // need QueryRegistryFeature feature to be added now in order to create the system database + system = std::make_unique(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, + 0, TRI_VOC_SYSTEM_DATABASE); + features.emplace_back(new arangodb::SystemDatabaseFeature(server, system.get()), + false); // required for IResearchAnalyzerFeature + features.emplace_back(new arangodb::TraverserEngineRegistryFeature(server), false); // must be before AqlFeature + features.emplace_back(new arangodb::AqlFeature(server), true); + features.emplace_back(new arangodb::aql::OptimizerRulesFeature(server), true); + features.emplace_back(new arangodb::aql::AqlFunctionFeature(server), true); // required for IResearchAnalyzerFeature + + for (auto& f : features) { + arangodb::application_features::ApplicationServer::server->addFeature(f.first); + } + + for (auto& f : features) { + f.first->prepare(); + } + + for (auto& f : features) { + if (f.second) { + f.first->start(); + } + } + + auto* dbPathFeature = + arangodb::application_features::ApplicationServer::getFeature( + "DatabasePath"); + arangodb::tests::setDatabasePath(*dbPathFeature); // ensure test data is stored in a unique directory + } + + ~Setup() { + system.reset(); // destroy before reseting the 'ENGINE' + arangodb::AqlFeature(server).stop(); // unset singleton instance + arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), + arangodb::LogLevel::DEFAULT); + arangodb::application_features::ApplicationServer::server = nullptr; + arangodb::EngineSelectorFeature::ENGINE = nullptr; + + // destroy application features + for (auto& f : features) { + if (f.second) { + f.first->stop(); + } + } + + for (auto& f : features) { + f.first->unprepare(); + } + } +}; // Setup + +struct MockGraphDatabase { + TRI_vocbase_t vocbase; + std::vector queries; + std::vector spos; + + MockGraphDatabase(std::string name) + : vocbase(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, 1, name) {} + + ~MockGraphDatabase() { + for (auto& q : queries) { + if (q->trx() != nullptr) { + q->trx()->abort(); + } + delete q; + } + for (auto& o : spos) { + delete o; + } + } + + // Create a collection with name and vertices + // with ids 0..n-1 + void addVertexCollection(std::string name, size_t n) { + std::shared_ptr vertices; + + auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + + "\", \"type\": 2 }"); + vertices = vocbase.createCollection(createJson->slice()); + REQUIRE((nullptr != vertices)); + + std::vector insertedDocs; + std::vector> docs; + for (size_t i = 0; i < n; i++) { + docs.emplace_back(arangodb::velocypack::Parser::fromJson( + "{ \"_key\": \"" + std::to_string(i) + "\"}")); + }; + + arangodb::OperationOptions options; + options.returnNew = true; + arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), + *vertices, arangodb::AccessMode::Type::WRITE); + CHECK((trx.begin().ok())); + + for (auto& entry : docs) { + auto res = trx.insert(vertices->name(), entry->slice(), options); + CHECK((res.ok())); + insertedDocs.emplace_back(res.slice().get("new")); + } + + CHECK((trx.commit().ok())); + CHECK(insertedDocs.size() == n); + CHECK(vertices->type()); + } + + // Create a collection with name of edges given by + void addEdgeCollection(std::string name, std::string vertexCollection, + std::vector> edgedef) { + std::shared_ptr edges; + auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + + "\", \"type\": 3 }"); + edges = vocbase.createCollection(createJson->slice()); + REQUIRE((nullptr != edges)); + + auto indexJson = velocypack::Parser::fromJson("{ \"type\": \"edge\" }"); + bool created = false; + auto index = edges->createIndex(indexJson->slice(), created); + CHECK(index); + CHECK(created); + + std::vector insertedDocs; + std::vector> docs; + + for (auto& p : edgedef) { + // std::cout << "edge: " << vertexCollection << " " << p.first << " -> " + // << p.second << std::endl; + auto docJson = velocypack::Parser::fromJson( + "{ \"_from\": \"" + vertexCollection + "/" + std::to_string(p.first) + + "\"" + ", \"_to\": \"" + vertexCollection + "/" + + std::to_string(p.second) + "\" }"); + docs.emplace_back(docJson); + } + + arangodb::OperationOptions options; + options.returnNew = true; + arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), + *edges, arangodb::AccessMode::Type::WRITE); + CHECK((trx.begin().ok())); + + for (auto& entry : docs) { + auto res = trx.insert(edges->name(), entry->slice(), options); + CHECK((res.ok())); + insertedDocs.emplace_back(res.slice().get("new")); + } + + CHECK((trx.commit().ok())); + CHECK(insertedDocs.size() == edgedef.size()); + } + + arangodb::aql::Query* getQuery(std::string qry) { + auto queryString = arangodb::aql::QueryString(qry); + + arangodb::aql::Query* query = + new arangodb::aql::Query(false, vocbase, queryString, nullptr, + arangodb::velocypack::Parser::fromJson("{}"), + arangodb::aql::PART_MAIN); + query->parse(); + query->prepare(arangodb::QueryRegistryFeature::registry()); + + queries.emplace_back(query); + + return query; + } + + arangodb::graph::ShortestPathOptions* getShortestPathOptions(arangodb::aql::Query* query) { + arangodb::graph::ShortestPathOptions* spo; + + auto plan = query->plan(); + auto ast = plan->getAst(); + + auto _toCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); + auto _fromCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); + + auto tmpVar = plan->getAst()->variables()->createTemporaryVariable(); + + AstNode* tmpId1 = plan->getAst()->createNodeReference(tmpVar); + AstNode* tmpId2 = plan->getAst()->createNodeValueString("", 0); + + { + auto const* access = + ast->createNodeAttributeAccess(tmpId1, StaticStrings::ToString.c_str(), + StaticStrings::ToString.length()); + auto const* cond = + ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); + _toCondition->addMember(cond); + } + + { + auto const* access = + ast->createNodeAttributeAccess(tmpId1, StaticStrings::FromString.c_str(), + StaticStrings::FromString.length()); + auto const* cond = + ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); + _fromCondition->addMember(cond); + } + spo = new ShortestPathOptions(query); + spo->setVariable(tmpVar); + spo->addLookupInfo(plan, "e", StaticStrings::FromString, _fromCondition->clone(ast)); + spo->addReverseLookupInfo(plan, "e", StaticStrings::ToString, _toCondition->clone(ast)); + + spos.emplace_back(spo); + return spo; + } +}; + +bool checkPath(ShortestPathOptions* spo, ShortestPathResult result, std::vector vertices, + std::vector> edges, std::string& msgs); + +} +} +} +#endif diff --git a/tests/Graph/KShortestPathsFinder.cpp b/tests/Graph/KShortestPathsFinder.cpp index 909f1ac23d..e04261d3b0 100644 --- a/tests/Graph/KShortestPathsFinder.cpp +++ b/tests/Graph/KShortestPathsFinder.cpp @@ -53,238 +53,13 @@ #include "../Mocks/Servers.h" #include "../Mocks/StorageEngineMock.h" #include "IResearch/common.h" +#include "GraphTestTools.h" using namespace arangodb; using namespace arangodb::aql; using namespace arangodb::graph; using namespace arangodb::velocypack; -namespace { -struct Setup { - StorageEngineMock engine; - arangodb::application_features::ApplicationServer server; - std::unique_ptr system; - std::vector> features; - - Setup() : engine(server), server(nullptr, nullptr) { - arangodb::EngineSelectorFeature::ENGINE = &engine; - arangodb::transaction::Methods::clearDataSourceRegistrationCallbacks(); - arangodb::ClusterEngine::Mocking = true; - arangodb::RandomGenerator::initialize(arangodb::RandomGenerator::RandomType::MERSENNE); - - // suppress log messages since tests check error conditions - arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), arangodb::LogLevel::ERR); // suppress WARNING DefaultCustomTypeHandler called - - // setup required application features - features.emplace_back(new arangodb::DatabasePathFeature(server), false); - features.emplace_back(new arangodb::DatabaseFeature(server), false); - features.emplace_back(new arangodb::QueryRegistryFeature(server), false); // must be first - arangodb::application_features::ApplicationServer::server->addFeature( - features.back().first); // need QueryRegistryFeature feature to be added now in order to create the system database - system = std::make_unique(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, - 0, TRI_VOC_SYSTEM_DATABASE); - features.emplace_back(new arangodb::SystemDatabaseFeature(server, system.get()), - false); // required for IResearchAnalyzerFeature - features.emplace_back(new arangodb::TraverserEngineRegistryFeature(server), false); // must be before AqlFeature - features.emplace_back(new arangodb::AqlFeature(server), true); - features.emplace_back(new arangodb::aql::OptimizerRulesFeature(server), true); - features.emplace_back(new arangodb::aql::AqlFunctionFeature(server), true); // required for IResearchAnalyzerFeature - - for (auto& f : features) { - arangodb::application_features::ApplicationServer::server->addFeature(f.first); - } - - for (auto& f : features) { - f.first->prepare(); - } - - for (auto& f : features) { - if (f.second) { - f.first->start(); - } - } - - auto* dbPathFeature = - arangodb::application_features::ApplicationServer::getFeature( - "DatabasePath"); - arangodb::tests::setDatabasePath(*dbPathFeature); // ensure test data is stored in a unique directory - } - - ~Setup() { - system.reset(); // destroy before reseting the 'ENGINE' - arangodb::AqlFeature(server).stop(); // unset singleton instance - arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), - arangodb::LogLevel::DEFAULT); - arangodb::application_features::ApplicationServer::server = nullptr; - arangodb::EngineSelectorFeature::ENGINE = nullptr; - - // destroy application features - for (auto& f : features) { - if (f.second) { - f.first->stop(); - } - } - - for (auto& f : features) { - f.first->unprepare(); - } - } -}; // Setup - -struct MockGraphDatabase { - TRI_vocbase_t vocbase; - std::vector queries; - std::vector spos; - - MockGraphDatabase(std::string name) - : vocbase(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, 1, name) {} - - ~MockGraphDatabase() { - for (auto& q : queries) { - if (q->trx() != nullptr) { - q->trx()->abort(); - } - delete q; - } - for (auto& o : spos) { - delete o; - } - } - - // Create a collection with name and vertices - // with ids 0..n-1 - void addVertexCollection(std::string name, size_t n) { - std::shared_ptr vertices; - - auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + - "\", \"type\": 2 }"); - vertices = vocbase.createCollection(createJson->slice()); - REQUIRE((nullptr != vertices)); - - std::vector insertedDocs; - std::vector> docs; - for (size_t i = 0; i < n; i++) { - docs.emplace_back(arangodb::velocypack::Parser::fromJson( - "{ \"_key\": \"" + std::to_string(i) + "\"}")); - }; - - arangodb::OperationOptions options; - options.returnNew = true; - arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), - *vertices, arangodb::AccessMode::Type::WRITE); - CHECK((trx.begin().ok())); - - for (auto& entry : docs) { - auto res = trx.insert(vertices->name(), entry->slice(), options); - CHECK((res.ok())); - insertedDocs.emplace_back(res.slice().get("new")); - } - - CHECK((trx.commit().ok())); - CHECK(insertedDocs.size() == n); - CHECK(vertices->type()); - } - - // Create a collection with name of edges given by - void addEdgeCollection(std::string name, std::string vertexCollection, - std::vector> edgedef) { - std::shared_ptr edges; - auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name + - "\", \"type\": 3 }"); - edges = vocbase.createCollection(createJson->slice()); - REQUIRE((nullptr != edges)); - - auto indexJson = velocypack::Parser::fromJson("{ \"type\": \"edge\" }"); - bool created = false; - auto index = edges->createIndex(indexJson->slice(), created); - CHECK(index); - CHECK(created); - - std::vector insertedDocs; - std::vector> docs; - - for (auto& p : edgedef) { - auto docJson = velocypack::Parser::fromJson( - "{ \"_from\": \"" + vertexCollection + "/" + std::to_string(p.first) + - "\"" + ", \"_to\": \"" + vertexCollection + "/" + - std::to_string(p.second) + "\" }"); - docs.emplace_back(docJson); - } - - arangodb::OperationOptions options; - options.returnNew = true; - arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase), - *edges, arangodb::AccessMode::Type::WRITE); - CHECK((trx.begin().ok())); - - for (auto& entry : docs) { - auto res = trx.insert(edges->name(), entry->slice(), options); - CHECK((res.ok())); - insertedDocs.emplace_back(res.slice().get("new")); - } - - CHECK((trx.commit().ok())); - CHECK(insertedDocs.size() == edgedef.size()); - } - - arangodb::aql::Query* getQuery(std::string qry) { - auto queryString = arangodb::aql::QueryString(qry); - - arangodb::aql::Query* query = - new arangodb::aql::Query(false, vocbase, queryString, nullptr, - arangodb::velocypack::Parser::fromJson("{}"), - arangodb::aql::PART_MAIN); - query->parse(); - query->prepare(arangodb::QueryRegistryFeature::registry()); - - queries.emplace_back(query); - - return query; - } - - arangodb::graph::ShortestPathOptions* getShortestPathOptions(arangodb::aql::Query* query) { - arangodb::graph::ShortestPathOptions* spo; - - auto plan = query->plan(); - auto ast = plan->getAst(); - - auto _toCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); - auto _fromCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND); - - auto tmpVar = plan->getAst()->variables()->createTemporaryVariable(); - - AstNode* tmpId1 = plan->getAst()->createNodeReference(tmpVar); - AstNode* tmpId2 = plan->getAst()->createNodeValueString("", 0); - - { - auto const* access = - ast->createNodeAttributeAccess(tmpId1, StaticStrings::ToString.c_str(), - StaticStrings::ToString.length()); - auto const* cond = - ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); - _toCondition->addMember(cond); - } - - { - auto const* access = - ast->createNodeAttributeAccess(tmpId1, StaticStrings::FromString.c_str(), - StaticStrings::FromString.length()); - auto const* cond = - ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2); - _fromCondition->addMember(cond); - } - spo = new ShortestPathOptions(query); - spo->setVariable(tmpVar); - spo->addLookupInfo(plan, "e", StaticStrings::FromString, _fromCondition->clone(ast)); - spo->addReverseLookupInfo(plan, "e", StaticStrings::ToString, _toCondition->clone(ast)); - - spos.emplace_back(spo); - return spo; - } -}; - -} // namespace - namespace arangodb { namespace tests { namespace graph { @@ -309,41 +84,8 @@ TEST_CASE("KShortestPathsFinder", "[graph]") { auto query = gdb.getQuery("RETURN 1"); auto spo = gdb.getShortestPathOptions(query); - auto checkPath = [spo](ShortestPathResult result, std::vector vertices, - std::vector> edges) -> bool { - bool res = true; - - if (result.length() != vertices.size()) return false; - - for (size_t i = 0; i < result.length(); i++) { - auto vert = result.vertexToAqlValue(spo->cache(), i); - AqlValueGuard guard(vert, true); - if (!vert.slice().get(StaticStrings::KeyString).isEqualString(vertices.at(i))) { - LOG_DEVEL << "expected vertex " << vertices.at(i) << " but found " - << vert.slice().get(StaticStrings::KeyString).toString(); - res = false; - } - } - - // First edge is by convention null - CHECK((result.edgeToAqlValue(spo->cache(), 0).isNull(true))); - for (size_t i = 1; i < result.length(); i++) { - auto edge = result.edgeToAqlValue(spo->cache(), i); - AqlValueGuard guard(edge, true); - if (!edge.slice().get(StaticStrings::FromString).isEqualString(edges.at(i).first) || - !edge.slice().get(StaticStrings::ToString).isEqualString(edges.at(i).second)) { - LOG_DEVEL << "expected edge " << edges.at(i).first << " -> " - << edges.at(i).second << " but found " - << edge.slice().get(StaticStrings::FromString).toString() << " -> " - << edge.slice().get(StaticStrings::ToString).toString(); - res = false; - } - } - return res; - }; auto finder = new KShortestPathsFinder(*spo); - SECTION("path from vertex to itself") { auto start = velocypack::Parser::fromJson("\"v/0\""); auto end = velocypack::Parser::fromJson("\"v/0\""); @@ -371,23 +113,27 @@ TEST_CASE("KShortestPathsFinder", "[graph]") { auto start = velocypack::Parser::fromJson("\"v/1\""); auto end = velocypack::Parser::fromJson("\"v/2\""); ShortestPathResult result; + std::string msgs; finder->startKShortestPathsTraversal(start->slice(), end->slice()); REQUIRE(true == finder->getNextPathShortestPathResult(result)); - REQUIRE(checkPath(result, {"1", "2"}, {{}, {"v/1", "v/2"}})); + auto cpr = checkPath(spo, result, {"1", "2"}, {{}, {"v/1", "v/2"}}, msgs); + REQUIRE_MESSAGE(cpr, msgs); } SECTION("path of length 4") { auto start = velocypack::Parser::fromJson("\"v/1\""); auto end = velocypack::Parser::fromJson("\"v/4\""); ShortestPathResult result; + std::string msgs; finder->startKShortestPathsTraversal(start->slice(), end->slice()); REQUIRE(true == finder->getNextPathShortestPathResult(result)); - REQUIRE(checkPath(result, {"1", "2", "3", "4"}, - {{}, {"v/1", "v/2"}, {"v/2", "v/3"}, {"v/3", "v/4"}})); + auto cpr = checkPath(spo, result, {"1", "2", "3", "4"}, + {{}, {"v/1", "v/2"}, {"v/2", "v/3"}, {"v/3", "v/4"}}, msgs); + REQUIRE_MESSAGE(cpr, msgs); } SECTION("path of length 5 with loops to start/end") { @@ -405,36 +151,43 @@ TEST_CASE("KShortestPathsFinder", "[graph]") { auto start = velocypack::Parser::fromJson("\"v/21\""); auto end = velocypack::Parser::fromJson("\"v/25\""); ShortestPathResult result; + std::string msgs; finder->startKShortestPathsTraversal(start->slice(), end->slice()); REQUIRE(true == finder->getNextPathShortestPathResult(result)); - CHECK((checkPath(result, {"21", "22", "23", "24", "25"}, - {{}, - {"v/21", "v/22"}, - {"v/22", "v/23"}, - {"v/23", "v/24"}, - {"v/24", "v/25"}}) || - checkPath(result, {"21", "26", "27", "28", "25"}, - {{}, - {"v/21", "v/26"}, - {"v/26", "v/27"}, - {"v/27", "v/28"}, - {"v/28", "v/25"}}))); + auto cpr = checkPath(spo, result, {"21", "22", "23", "24", "25"}, + {{}, + {"v/21", "v/22"}, + {"v/22", "v/23"}, + {"v/23", "v/24"}, + {"v/24", "v/25"}}, + msgs) || + checkPath(spo, result, {"21", "26", "27", "28", "25"}, + {{}, + {"v/21", "v/26"}, + {"v/26", "v/27"}, + {"v/27", "v/28"}, + {"v/28", "v/25"}}, + msgs); + REQUIRE_MESSAGE(cpr, msgs); REQUIRE(true == finder->getNextPathShortestPathResult(result)); - CHECK((checkPath(result, {"21", "22", "23", "24", "25"}, - {{}, - {"v/21", "v/22"}, - {"v/22", "v/23"}, - {"v/23", "v/24"}, - {"v/24", "v/25"}}) || - checkPath(result, {"21", "26", "27", "28", "25"}, - {{}, - {"v/21", "v/26"}, - {"v/26", "v/27"}, - {"v/27", "v/28"}, - {"v/28", "v/25"}}))); -} + cpr = checkPath(spo, result, {"21", "22", "23", "24", "25"}, + {{}, + {"v/21", "v/22"}, + {"v/22", "v/23"}, + {"v/23", "v/24"}, + {"v/24", "v/25"}}, + msgs) || + checkPath(spo, result, {"21", "26", "27", "28", "25"}, + {{}, + {"v/21", "v/26"}, + {"v/26", "v/27"}, + {"v/27", "v/28"}, + {"v/28", "v/25"}}, + msgs); + REQUIRE_MESSAGE(cpr, msgs); + } SECTION("many edges between two nodes") { auto start = velocypack::Parser::fromJson("\"v/70\"");