From 2bd0822155a966f06f6c2bbf28bdbc807350eaaa Mon Sep 17 00:00:00 2001 From: Marko Culinovic Date: Tue, 2 Oct 2018 12:36:11 +0200 Subject: [PATCH] Implement console client for memgraph Reviewers: teon.banek, mferencevic Reviewed By: teon.banek, mferencevic Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1616 --- src/communication/bolt/v1/value.cpp | 57 ++- tools/src/CMakeLists.txt | 14 +- tools/src/mg_client/main.cpp | 451 ++++++++++++++++++ tools/tests/client/input_escaping.txt | 1 + tools/tests/client/input_multiline_query.txt | 16 + .../input_multiple_queries_per_line.txt | 1 + tools/tests/client/input_query_per_line.txt | 4 + tools/tests/client/input_quote.txt | 5 + tools/tests/client/input_unfinished_query.txt | 6 + tools/tests/client/output_escaping.txt | 9 + tools/tests/client/output_multiline_query.txt | 49 ++ .../output_multiple_queries_per_line.txt | 19 + tools/tests/client/output_query_per_line.txt | 27 ++ tools/tests/client/output_quote.txt | 33 ++ .../tests/client/output_unfinished_query.txt | 39 ++ tools/tests/test_mg_client | 106 ++++ 16 files changed, 815 insertions(+), 22 deletions(-) create mode 100644 tools/src/mg_client/main.cpp create mode 100644 tools/tests/client/input_escaping.txt create mode 100644 tools/tests/client/input_multiline_query.txt create mode 100644 tools/tests/client/input_multiple_queries_per_line.txt create mode 100644 tools/tests/client/input_query_per_line.txt create mode 100644 tools/tests/client/input_quote.txt create mode 100644 tools/tests/client/input_unfinished_query.txt create mode 100644 tools/tests/client/output_escaping.txt create mode 100644 tools/tests/client/output_multiline_query.txt create mode 100644 tools/tests/client/output_multiple_queries_per_line.txt create mode 100644 tools/tests/client/output_query_per_line.txt create mode 100644 tools/tests/client/output_quote.txt create mode 100644 tools/tests/client/output_unfinished_query.txt create mode 100755 tools/tests/test_mg_client diff --git a/src/communication/bolt/v1/value.cpp b/src/communication/bolt/v1/value.cpp index 364f44958..e2f4a01c7 100644 --- a/src/communication/bolt/v1/value.cpp +++ b/src/communication/bolt/v1/value.cpp @@ -253,35 +253,50 @@ Value::~Value() { } std::ostream &operator<<(std::ostream &os, const Vertex &vertex) { - os << "V("; + os << "("; + if (vertex.labels.size() > 0) { + os << ":"; + } utils::PrintIterable(os, vertex.labels, ":", [&](auto &stream, auto label) { stream << label; }); - os << " {"; - utils::PrintIterable(os, vertex.properties, ", ", - [&](auto &stream, const auto &pair) { - stream << pair.first << ": " << pair.second; - }); - return os << "})"; + if (vertex.labels.size() > 0 && vertex.properties.size() > 0) { + os << " "; + } + if (vertex.properties.size() > 0) { + os << "{"; + utils::PrintIterable(os, vertex.properties, ", ", + [&](auto &stream, const auto &pair) { + stream << pair.first << ": " << pair.second; + }); + os << "}"; + } + return os << ")"; } std::ostream &operator<<(std::ostream &os, const Edge &edge) { - os << "E[" << edge.type; - os << " {"; - utils::PrintIterable(os, edge.properties, ", ", - [&](auto &stream, const auto &pair) { - stream << pair.first << ": " << pair.second; - }); - return os << "}]"; + os << "[" << edge.type; + if (edge.properties.size() > 0) { + os << " {"; + utils::PrintIterable(os, edge.properties, ", ", + [&](auto &stream, const auto &pair) { + stream << pair.first << ": " << pair.second; + }); + os << "}"; + } + return os << "]"; } std::ostream &operator<<(std::ostream &os, const UnboundedEdge &edge) { - os << "E[" << edge.type; - os << " {"; - utils::PrintIterable(os, edge.properties, ", ", - [&](auto &stream, const auto &pair) { - stream << pair.first << ": " << pair.second; - }); - return os << "}]"; + os << "[" << edge.type; + if (edge.properties.size() > 0) { + os << " {"; + utils::PrintIterable(os, edge.properties, ", ", + [&](auto &stream, const auto &pair) { + stream << pair.first << ": " << pair.second; + }); + os << "}"; + } + return os << "]"; } std::ostream &operator<<(std::ostream &os, const Path &path) { diff --git a/tools/src/CMakeLists.txt b/tools/src/CMakeLists.txt index 8087c990d..8b0da0fa3 100644 --- a/tools/src/CMakeLists.txt +++ b/tools/src/CMakeLists.txt @@ -6,15 +6,27 @@ target_link_libraries(mg_import_csv memgraph_lib kvstore_dummy_lib) add_executable(mg_statsd mg_statsd/main.cpp) target_link_libraries(mg_statsd memgraph_lib kvstore_dummy_lib) +# Memgraph Client Target +add_executable(mg_client mg_client/main.cpp) +set(CLIENT_LIBS mg-communication mg-io mg-utils) +if (READLINE_FOUND) + list(APPEND CLIENT_LIBS readline) +endif() +target_link_libraries(mg_client ${CLIENT_LIBS}) + # Strip the executable in release build. string(TOLOWER ${CMAKE_BUILD_TYPE} lower_build_type) if (lower_build_type STREQUAL "release") add_custom_command(TARGET mg_import_csv POST_BUILD COMMAND strip -s mg_import_csv COMMENT Stripping symbols and sections from mg_import_csv) + add_custom_command(TARGET mg_client POST_BUILD + COMMAND strip -s mg_client + COMMENT Stripping symbols and sections from mg_client) endif() install(TARGETS mg_import_csv RUNTIME DESTINATION bin) +install(TARGETS mg_client RUNTIME DESTINATION bin) # Target for building all the tool executables. -add_custom_target(tools DEPENDS mg_import_csv mg_statsd) +add_custom_target(tools DEPENDS mg_import_csv mg_statsd mg_client) diff --git a/tools/src/mg_client/main.cpp b/tools/src/mg_client/main.cpp new file mode 100644 index 000000000..5de91a0fb --- /dev/null +++ b/tools/src/mg_client/main.cpp @@ -0,0 +1,451 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "communication/bolt/client.hpp" +#include "io/network/endpoint.hpp" +#include "io/network/utils.hpp" +#include "utils/file.hpp" +#include "utils/string.hpp" + +namespace fs = std::experimental::filesystem; + +DEFINE_string(host, "127.0.0.1", + "Server address. It can be a DNS resolveable hostname."); +DEFINE_int32(port, 7687, "Server port"); +DEFINE_string(username, "", "Username for the database"); +DEFINE_string(password, "", "Password for the database"); +DEFINE_bool(use_ssl, true, "Set to true to connect with SSL to the server."); +DEFINE_bool(fit_to_screen, false, "Fit output width to screen width."); + +#ifdef HAS_READLINE +DEFINE_string(history, "~/.memgraph", + "Use the specified directory for saving history."); +DEFINE_bool(no_history, false, "Do not save history."); + +// History default directory. +static const std::string kDefaultHistoryBaseDir = "~"; +static const std::string kDefaultHistoryMemgraphDir = ".memgraph"; +// History filename. +static const std::string kHistoryFilename = "client_history"; +#endif + +DECLARE_int32(min_log_level); + +// Unfinished query text from previous input. +// e.g. Previous input was MATCH(n) RETURN n; MATCH +// then default_text would be set to MATCH for next query. +std::string default_text; + +#ifdef HAS_READLINE + +#include "readline/history.h" +#include "readline/readline.h" + +/** Helper function that sets default input for 'readline'*/ +static int SetDefaultText() { + rl_insert_text(default_text.c_str()); + default_text = ""; + rl_startup_hook = (rl_hook_func_t *)NULL; + return 0; +} + +/** Memgraph and OpenCypher keywords. + * Copied from src/query/frontend/opencypher/grammar/Cypher.g4 + * and src/query/frontend/grammar/MemgraphCypher.g4 */ +static const std::vector kMemgraphKeywords{ + "ALTER", "AUTH", "BATCH", "BATCHES", "CLEAR", "DATA", + "DENY", "DROP", "FOR", "FROM", "GRANT", "IDENTIFIED", + "INTERVAL", "K_TEST", "KAFKA", "LOAD", "PASSWORD", "PRIVILEGES", + "REVOKE", "ROLE", "ROLES", "SIZE", "START", "STOP", + "STREAM", "STREAMS", "TO", "TOPIC", "TRANSFORM", "USER", + "USERS"}; +static const std::vector kCypherKeywords{ + "ALL", "AND", "ANY", "AS", "ASC", "ASCENDING", + "BFS", "BY", "CASE", "CONTAINS", "COUNT", "CREATE", + "CYPHERNULL", "DELETE", "DESC", "DESCENDING", "DETACH", "DISTINCT", + "ELSE", "END", "ENDS", "EXTRACT", "FALSE", "FILTER", + "IN", "INDEX", "IS", "LIMIT", "L_SKIP", "MATCH", + "MERGE", "NONE", "NOT", "ON", "OPTIONAL", "OR", + "ORDER", "REDUCE", "REMOVE", "RETURN", "SET", "SHOW", + "SINGLE", "STARTS", "THEN", "TRUE", "UNION", "UNWIND", + "WHEN", "WHERE", "WITH", "WSHORTEST", "XOR"}; + +static char *CompletionGenerator(const char *text, int state) { + // This function is called with state=0 the first time; subsequent calls + // are with a nonzero state. state=0 can be used to perform one-time + // initialization for this completion session. + static std::vector matches; + static size_t match_index = 0; + + if (state == 0) { + // During initialization, compute the actual matches for 'text' and + // keep them in a static vector. + matches.clear(); + match_index = 0; + + // Collect a vector of matches: vocabulary words that begin with text. + std::string text_str = utils::ToUpperCase(std::string(text)); + for (auto word : kCypherKeywords) { + if (word.size() >= text_str.size() && + word.compare(0, text_str.size(), text_str) == 0) { + matches.push_back(word); + } + } + for (auto word : kMemgraphKeywords) { + if (word.size() >= text_str.size() && + word.compare(0, text_str.size(), text_str) == 0) { + matches.push_back(word); + } + } + } + + if (match_index >= matches.size()) { + // We return nullptr to notify the caller no more matches are available. + return nullptr; + } else { + // Return a malloc'd char* for the match. The caller frees it. + return strdup(matches[match_index++].c_str()); + } +} + +static char **Completer(const char *text, int start, int end) { + // Don't do filename completion even if our generator finds no matches. + rl_attempted_completion_over = 1; + // Note: returning nullptr here will make readline use the default filename + // completer. This note is copied from examples - I think because + // rl_attempted_completion_over is set to 1, filename completer won't be used. + return rl_completion_matches(text, CompletionGenerator); +} + +/** + * Helper function that reads a line from the + * standard input using the 'readline' lib. + * Adds support for history and reverse-search. + * + * @param prompt The prompt to display. + * @return User input line, or nullopt on EOF. + */ +static std::experimental::optional ReadLine( + const std::string &prompt) { + if (default_text.size() > 0) { + // Initialize text with remainder of previous query. + rl_startup_hook = SetDefaultText; + } + char *line = readline(prompt.c_str()); + if (!line) return std::experimental::nullopt; + + std::string r_val(line); + if (!utils::Trim(r_val).empty()) add_history(line); + free(line); + return r_val; +} + +#else + +/** Helper function that reads a line from the standard input + * using getline. + * @param prompt The prompt to display. + * @return User input line, or nullopt on EOF. + */ +static std::experimental::optional ReadLine( + const std::string &prompt) { + std::cout << prompt << default_text; + std::string line; + std::getline(std::cin, line); + if (!isatty(STDIN_FILENO)) { + // Stupid hack to have same output as readline if stdin is redirected. + std::cout << line << std::endl; + } + line = default_text + line; + default_text = ""; + if (std::cin.eof()) return std::experimental::nullopt; + return line; +} + +#endif // HAS_READLINE + +/** Helper function that parses user line input. + * @param line user input line. + * @param quote quote character or '\0'; if set line is inside quotation. + * @param escaped if set, next character should be escaped. + * @return pair of string and bool. string is parsed line and bool marks + * if query finished(Query finishes with ';') with this line. + */ +static std::pair ParseLine(const std::string &line, + char *quote, bool *escaped) { + // Parse line. + bool is_done = false; + std::stringstream parsed_line; + for (auto c : line) { + if (*quote && c == '\\') { + // Escaping is only used inside quotation to not end the quote + // when quotation char is escaped. + *escaped = !*escaped; + parsed_line << c; + continue; + } else if ((!*quote && (c == '\"' || c == '\'')) || + (!*escaped && c == *quote)) { + *quote = *quote ? '\0' : c; + } else if (!*quote && c == ';') { + is_done = true; + break; + } + parsed_line << c; + *escaped = false; + } + return std::make_pair(parsed_line.str(), is_done); +} + +static std::experimental::optional GetQuery( + const std::string &prompt) { + char quote = '\0'; + bool escaped = false; + auto ret = ParseLine(default_text, "e, &escaped); + if (ret.second) { + auto idx = ret.first.size() + 1; + default_text = utils::Trim(default_text.substr(idx)); + return ret.first; + } + std::stringstream query; + std::string multiline_prompt(" -> "); + int line_cnt = 0; + auto is_done = false; + while (!is_done) { + auto line = ReadLine(line_cnt == 0 ? prompt : multiline_prompt); + if (!line) return std::experimental::nullopt; + if (line->empty()) continue; + auto ret = ParseLine(*line, "e, &escaped); + query << ret.first << " "; + auto char_count = ret.first.size(); + if (ret.second) { + is_done = true; + char_count += 1; + } + if (char_count < line->size()) { + default_text = utils::Trim(line->substr(char_count)); + } + ++line_cnt; + } + return query.str(); +} + +template +static void PrettyPrintData(const std::vector &data, int total_width, + int column_width, int num_columns, + bool all_columns_fit) { + if (!all_columns_fit) num_columns -= 1; + std::string data_output = std::string(total_width, ' '); + for (auto i = 0; i < total_width; i += column_width) { + data_output[i] = '|'; + int idx = i / column_width; + if (idx < num_columns) { + std::stringstream field; + field << data[idx]; // convert Value to string + std::string field_str(field.str()); + if (field_str.size() > column_width - 1) { + field_str.erase(column_width - 1, std::string::npos); + field_str.replace(field_str.size() - 3, 3, "..."); + } + data_output.replace(i + 1, field_str.size(), field_str); + } + } + if (!all_columns_fit) { + data_output.replace(total_width - column_width, 3, "..."); + } + data_output[total_width - 1] = '|'; + std::cout << data_output << std::endl; +} + +/** + * Helper function for determining maximum length of data. + * @param data Vector of string representable elements. Elements should have + * operator '<<' implemented. + * @return length needed for representing max size element in vector. Plus + * one is added because of column start character '|'. + */ +template +static uint64_t GetMaxColumnWidth(const std::vector &data) { + uint64_t column_width = 0; + for (auto &elem : data) { + std::stringstream field; + field << elem; + column_width = std::max(column_width, field.str().size()); + } + return column_width + 1; +} + +static void PrettyPrint( + const std::vector &header, + const std::vector> &records) { + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + bool all_columns_fit = true; + + auto num_columns = header.size(); + auto column_width = GetMaxColumnWidth(header); + for (size_t i = 0; i < records.size(); ++i) { + column_width = std::max(column_width, GetMaxColumnWidth(records[i])); + } + column_width = std::max(static_cast(5), + column_width); // set column width to min 5 + auto total_width = column_width * num_columns + 1; + + // Fit to screen width. + if (FLAGS_fit_to_screen && total_width > w.ws_col) { + uint64_t lo = 5; + uint64_t hi = column_width; + uint64_t last = 5; + while (lo < hi) { + uint64_t mid = lo + (hi - lo) / 2; + uint64_t width = mid * num_columns + 1; + if (width <= w.ws_col) { + last = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + column_width = last; + total_width = column_width * num_columns + 1; + // All columns do not fit on screen. + while (total_width > w.ws_col && num_columns > 1) { + num_columns -= 1; + total_width = column_width * num_columns + 1; + all_columns_fit = false; + } + } + + auto line_fill = std::string(total_width, '-'); + for (auto i = 0; i < total_width; i += column_width) { + line_fill[i] = '+'; + } + line_fill[total_width - 1] = '+'; + std::cout << line_fill << std::endl; + // Print Header. + PrettyPrintData(header, total_width, column_width, num_columns, + all_columns_fit); + std::cout << line_fill << std::endl; + // Print Records. + for (size_t i = 0; i < records.size(); ++i) { + PrettyPrintData(records[i], total_width, column_width, num_columns, + all_columns_fit); + } + std::cout << line_fill << std::endl; +} + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + FLAGS_min_log_level = google::ERROR; + google::InitGoogleLogging(argv[0]); + + communication::Init(); + +#ifdef HAS_READLINE + using_history(); + int history_len = 0; + rl_attempted_completion_function = Completer; + fs::path history_dir = FLAGS_history; + if (FLAGS_history == + (kDefaultHistoryBaseDir + "/" + kDefaultHistoryMemgraphDir)) { + // Fetch home dir for user. + struct passwd *pw = getpwuid(getuid()); + history_dir = fs::path(pw->pw_dir) / kDefaultHistoryMemgraphDir; + } + if (!utils::EnsureDir(history_dir)) { + std::cout << fmt::format("History directory {} doesn't exist", history_dir) + << std::endl; + // Should program exit here or just continue with warning message? + return 1; + } + fs::path history_file = history_dir / kHistoryFilename; + // Read history file. + if (fs::exists(history_file)) { + auto ret = read_history(history_file.string().c_str()); + if (ret != 0) { + std::cout << fmt::format("Unable to read history file: {}", history_file); + // Should program exit here or just continue with warning message? + return 1; + } + history_len = history_length; + } +#endif + + // TODO handle endpoint exception. + // It has CHECK in constructor if address is not valid. + io::network::Endpoint endpoint(io::network::ResolveHostname(FLAGS_host), + FLAGS_port); + communication::ClientContext context(FLAGS_use_ssl); + communication::bolt::Client client(&context); + + if (!client.Connect(endpoint, FLAGS_username, FLAGS_password)) { + // Error message is logged in client.Connect method + return 1; + } + + std::cout << "mg-client" << std::endl; + std::cout << fmt::format("Connected to 'memgraph://{}'", endpoint) + << std::endl; + int num_retries = 3; + while (true) { + auto query = GetQuery("memgraph> "); + if (!query) break; + if (query->empty()) continue; + try { + auto ret = client.Execute(*query, {}); + PrettyPrint(ret.fields, ret.records); + } catch (const communication::bolt::ClientQueryException &e) { + std::cout << "Client received exception: " << e.what() << std::endl; + } catch (const communication::bolt::ClientFatalException &e) { + std::cout << "Client received exception: " << e.what() << std::endl; + std::cout << "Trying to reconnect" << std::endl; + bool is_connected = false; + client.Close(); + while (num_retries > 0) { + --num_retries; + if (client.Connect(endpoint, FLAGS_username, FLAGS_password)) { + is_connected = true; + break; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + if (is_connected) { + num_retries = 3; + std::cout << fmt::format("Connected to 'memgraph://{}'", endpoint) + << std::endl; + } else { + std::cout << fmt::format("Couldn't connect to 'memgraph://{}'", + endpoint) + << std::endl; + return 1; + } + } + } + +#if HAS_READLINE + int curr_len = history_length; + if (!FLAGS_no_history && curr_len - history_len > 0) { + int ret = 0; + // If there was no history, create history file. + // Otherwise, append to existing history. + if (history_len == 0) { + ret = write_history(history_file.string().c_str()); + } else { + ret = + append_history(curr_len - history_len, history_file.string().c_str()); + } + if (ret != 0) { + std::cout << "Unable to save history" << std::endl; + return 1; + } + } +#endif + return 0; +} diff --git a/tools/tests/client/input_escaping.txt b/tools/tests/client/input_escaping.txt new file mode 100644 index 000000000..c0a686a8c --- /dev/null +++ b/tools/tests/client/input_escaping.txt @@ -0,0 +1 @@ +CREATE (n:Node{tmp:"\"\\;\\"}) RETURN n; diff --git a/tools/tests/client/input_multiline_query.txt b/tools/tests/client/input_multiline_query.txt new file mode 100644 index 000000000..7d36d2939 --- /dev/null +++ b/tools/tests/client/input_multiline_query.txt @@ -0,0 +1,16 @@ +CREATE (n:Constantine{quote:"In hoc signo vinces"}) +RETURN n; +MATCH +(n) +RETURN +n; +CREATE (n:Erdody{quote: +"Regnum regno non praescribit leges"}) +RETURN +n; +MATCH (n:Erdody) RETURN +n; +CREATE (n:Caesar{quote:"Alea iacta +est"}) RETURN n; +MATCH (n:Caesar) +RETURN n; diff --git a/tools/tests/client/input_multiple_queries_per_line.txt b/tools/tests/client/input_multiple_queries_per_line.txt new file mode 100644 index 000000000..2f215061d --- /dev/null +++ b/tools/tests/client/input_multiple_queries_per_line.txt @@ -0,0 +1 @@ +CREATE (n:Node) RETURN n; CREATE (n:Vertex) RETURN n; MATCH (n:Vertex) RETURN n; diff --git a/tools/tests/client/input_query_per_line.txt b/tools/tests/client/input_query_per_line.txt new file mode 100644 index 000000000..34b8045f2 --- /dev/null +++ b/tools/tests/client/input_query_per_line.txt @@ -0,0 +1,4 @@ +CREATE (n:Node) RETURN n; +MATCH (n:Node) RETURN n; +CREATE (n:Vertex) RETURN n; +MATCH (n:Vertex) RETURN n; diff --git a/tools/tests/client/input_quote.txt b/tools/tests/client/input_quote.txt new file mode 100644 index 000000000..833db85a7 --- /dev/null +++ b/tools/tests/client/input_quote.txt @@ -0,0 +1,5 @@ +CREATE (n:Ciceron{quote:"o tempora o mores"}) RETURN n; +CREATE (n:Ciceron{quote:'o tempora o mores!'}) RETURN n; +CREATE (n:Ciceron{quote:"o tempora 'o mores'"}) RETURN n; +CREATE (n:Ciceron{quote:'o tempora "o mores"'}) RETURN n; +CREATE (n:Ciceron{quote:"o tempora \"o mores\""}) RETURN n; diff --git a/tools/tests/client/input_unfinished_query.txt b/tools/tests/client/input_unfinished_query.txt new file mode 100644 index 000000000..f2dca2af0 --- /dev/null +++ b/tools/tests/client/input_unfinished_query.txt @@ -0,0 +1,6 @@ +CREATE (n:Ovid{quote:"Exitus Acta Probat"}) RETURN n; MATCH (n) +RETURN n; +MATCH (n) RETURN n; CREATE (n:Bible{quote:"Fiat Lux"}) RETURN n; CREATE (n:Plinius{quote:"In vino veritas"}) RETURN n; MATCH +(n:Plinius) +RETURN +n; diff --git a/tools/tests/client/output_escaping.txt b/tools/tests/client/output_escaping.txt new file mode 100644 index 000000000..774cfcce5 --- /dev/null +++ b/tools/tests/client/output_escaping.txt @@ -0,0 +1,9 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Node{tmp:"\"\\;\\"}) RETURN n; ++-------------------+ +|n | ++-------------------+ +|(:Node {tmp: "\;\})| ++-------------------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/client/output_multiline_query.txt b/tools/tests/client/output_multiline_query.txt new file mode 100644 index 000000000..1af7de50f --- /dev/null +++ b/tools/tests/client/output_multiline_query.txt @@ -0,0 +1,49 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Constantine{quote:"In hoc signo vinces"}) + -> RETURN n; ++-------------------------------------------+ +|n | ++-------------------------------------------+ +|(:Constantine {quote: In hoc signo vinces})| ++-------------------------------------------+ +memgraph> MATCH + -> (n) + -> RETURN + -> n; ++-------------------------------------------+ +|n | ++-------------------------------------------+ +|(:Constantine {quote: In hoc signo vinces})| ++-------------------------------------------+ +memgraph> CREATE (n:Erdody{quote: + -> "Regnum regno non praescribit leges"}) + -> RETURN + -> n; ++-----------------------------------------------------+ +|n | ++-----------------------------------------------------+ +|(:Erdody {quote: Regnum regno non praescribit leges})| ++-----------------------------------------------------+ +memgraph> MATCH (n:Erdody) RETURN + -> n; ++-----------------------------------------------------+ +|n | ++-----------------------------------------------------+ +|(:Erdody {quote: Regnum regno non praescribit leges})| ++-----------------------------------------------------+ +memgraph> CREATE (n:Caesar{quote:"Alea iacta + -> est"}) RETURN n; ++---------------------------------+ +|n | ++---------------------------------+ +|(:Caesar {quote: Alea iacta est})| ++---------------------------------+ +memgraph> MATCH (n:Caesar) + -> RETURN n; ++---------------------------------+ +|n | ++---------------------------------+ +|(:Caesar {quote: Alea iacta est})| ++---------------------------------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/client/output_multiple_queries_per_line.txt b/tools/tests/client/output_multiple_queries_per_line.txt new file mode 100644 index 000000000..c6babb8ad --- /dev/null +++ b/tools/tests/client/output_multiple_queries_per_line.txt @@ -0,0 +1,19 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Node) RETURN n; CREATE (n:Vertex) RETURN n; MATCH (n:Vertex) RETURN n; ++-------+ +|n | ++-------+ +|(:Node)| ++-------+ ++---------+ +|n | ++---------+ +|(:Vertex)| ++---------+ ++---------+ +|n | ++---------+ +|(:Vertex)| ++---------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/client/output_query_per_line.txt b/tools/tests/client/output_query_per_line.txt new file mode 100644 index 000000000..2e16ae908 --- /dev/null +++ b/tools/tests/client/output_query_per_line.txt @@ -0,0 +1,27 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Node) RETURN n; ++-------+ +|n | ++-------+ +|(:Node)| ++-------+ +memgraph> MATCH (n:Node) RETURN n; ++-------+ +|n | ++-------+ +|(:Node)| ++-------+ +memgraph> CREATE (n:Vertex) RETURN n; ++---------+ +|n | ++---------+ +|(:Vertex)| ++---------+ +memgraph> MATCH (n:Vertex) RETURN n; ++---------+ +|n | ++---------+ +|(:Vertex)| ++---------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/client/output_quote.txt b/tools/tests/client/output_quote.txt new file mode 100644 index 000000000..ac6353638 --- /dev/null +++ b/tools/tests/client/output_quote.txt @@ -0,0 +1,33 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Ciceron{quote:"o tempora o mores"}) RETURN n; ++-------------------------------------+ +|n | ++-------------------------------------+ +|(:Ciceron {quote: o tempora o mores})| ++-------------------------------------+ +memgraph> CREATE (n:Ciceron{quote:'o tempora o mores!'}) RETURN n; ++--------------------------------------+ +|n | ++--------------------------------------+ +|(:Ciceron {quote: o tempora o mores!})| ++--------------------------------------+ +memgraph> CREATE (n:Ciceron{quote:"o tempora 'o mores'"}) RETURN n; ++---------------------------------------+ +|n | ++---------------------------------------+ +|(:Ciceron {quote: o tempora 'o mores'})| ++---------------------------------------+ +memgraph> CREATE (n:Ciceron{quote:'o tempora "o mores"'}) RETURN n; ++---------------------------------------+ +|n | ++---------------------------------------+ +|(:Ciceron {quote: o tempora "o mores"})| ++---------------------------------------+ +memgraph> CREATE (n:Ciceron{quote:"o tempora \"o mores\""}) RETURN n; ++---------------------------------------+ +|n | ++---------------------------------------+ +|(:Ciceron {quote: o tempora "o mores"})| ++---------------------------------------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/client/output_unfinished_query.txt b/tools/tests/client/output_unfinished_query.txt new file mode 100644 index 000000000..9ac92a7f2 --- /dev/null +++ b/tools/tests/client/output_unfinished_query.txt @@ -0,0 +1,39 @@ +mg-client +Connected to 'memgraph://127.0.0.1:7687' +memgraph> CREATE (n:Ovid{quote:"Exitus Acta Probat"}) RETURN n; MATCH (n) ++-----------------------------------+ +|n | ++-----------------------------------+ +|(:Ovid {quote: Exitus Acta Probat})| ++-----------------------------------+ +memgraph> MATCH (n)RETURN n; ++-----------------------------------+ +|n | ++-----------------------------------+ +|(:Ovid {quote: Exitus Acta Probat})| ++-----------------------------------+ +memgraph> MATCH (n) RETURN n; CREATE (n:Bible{quote:"Fiat Lux"}) RETURN n; CREATE (n:Plinius{quote:"In vino veritas"}) RETURN n; MATCH ++-----------------------------------+ +|n | ++-----------------------------------+ +|(:Ovid {quote: Exitus Acta Probat})| ++-----------------------------------+ ++--------------------------+ +|n | ++--------------------------+ +|(:Bible {quote: Fiat Lux})| ++--------------------------+ ++-----------------------------------+ +|n | ++-----------------------------------+ +|(:Plinius {quote: In vino veritas})| ++-----------------------------------+ +memgraph> MATCH(n:Plinius) + -> RETURN + -> n; ++-----------------------------------+ +|n | ++-----------------------------------+ +|(:Plinius {quote: In vino veritas})| ++-----------------------------------+ +memgraph> \ No newline at end of file diff --git a/tools/tests/test_mg_client b/tools/tests/test_mg_client new file mode 100755 index 000000000..166b6eea6 --- /dev/null +++ b/tools/tests/test_mg_client @@ -0,0 +1,106 @@ +#!/bin/bash + +## Helper functions + +function wait_for_server { + port=$1 + while ! nc -z -w 1 127.0.0.1 $port; do + sleep 0.1 + done + sleep 1 +} + +function echo_info { printf "\033[1;36m~~ $1 ~~\033[0m\n"; } +function echo_success { printf "\033[1;32m~~ $1 ~~\033[0m\n\n"; } +function echo_failure { printf "\033[1;31m~~ $1 ~~\033[0m\n\n"; } + + +## Environment setup +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$DIR" + +# Create a temporary directory for output files +tmpdir=/tmp/mg_client/output +if [ -d $tmpdir ]; then + rm -rf $tmpdir +fi +mkdir -p $tmpdir +cd $tmpdir + +# Find memgraph binaries. +memgraph_dir="$DIR/../../build" +if [ ! -d $memgraph_dir ]; then + memgraph_dir="$DIR/../../build_release" +fi + +# Find mg_client binaries. +client_dir="$memgraph_dir/tools/src" +if [ ! -d $client_dir ]; then + echo_failure "mg-client directory not found" + exit 1 +fi + +# Find tests input files. +tests_dir="$DIR/client" +if [ ! -d $tests_dir ]; then + echo_failure "Directory with tests input not found" + exit 1 +fi + + +## Startup + +# Start the memgraph process and wait for it to start. +echo_info "Starting memgraph" +$memgraph_dir/memgraph &> /dev/null & +pid=$! +wait_for_server 7687 +echo_success "Started memgraph" + + +## Tests + +echo_info "Running tests" +echo # Blank line + +client_flags="--use-ssl=false" +test_code=0 +for filename in $tests_dir/input_*.txt; do + test_name=$(basename $filename) + test_name=${test_name#*_} + test_name=${test_name%.*} + output_name="output_$test_name.txt" + + echo_info "Running test $test_name" + $client_dir/mg_client $client_flags < $filename > $tmpdir/$test_name + diff -b $tmpdir/$test_name $tests_dir/$output_name + test_code=$? + if [ $test_code -ne 0 ]; then + echo_failure "Test $test_name failed" + break + else + echo_success "Test $test_name passed" + fi + + # Clear database for each test. + $client_dir/mg_client $client_flags <<< "MATCH (n) DETACH DELETE n;" \ + &> /dev/null || exit 1 +done + + +## Cleanup +echo_info "Starting test cleanup" + +# Shutdown the memgraph process. +kill $pid +wait -n +code_mg=$? + +# Check memgraph exit code. +if [ $code_mg -ne 0 ]; then + echo_failure "The memgraph process didn't terminate properly!" + exit $code_mg +fi +echo_success "Test cleanup done" + +exit $test_code