diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 29b15a9c5..33d397754 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -2391,6 +2391,9 @@ cpp<# (lcp:define-enum sync-mode (sync async) (:serialize)) + (lcp:define-enum replica-state + (ready replicating recovery invalid) + (:serialize)) #>cpp ReplicationQuery() = default; diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index 8a7ba6ed8..80480f2d7 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -231,7 +231,20 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler { if (repl_info.timeout) { replica.timeout = *repl_info.timeout; } - + switch (repl_info.state) { + case storage::replication::ReplicaState::READY: + replica.state = ReplicationQuery::ReplicaState::READY; + break; + case storage::replication::ReplicaState::REPLICATING: + replica.state = ReplicationQuery::ReplicaState::REPLICATING; + break; + case storage::replication::ReplicaState::RECOVERY: + replica.state = ReplicationQuery::ReplicaState::RECOVERY; + break; + case storage::replication::ReplicaState::INVALID: + replica.state = ReplicationQuery::ReplicaState::INVALID; + break; + } return replica; }; @@ -486,6 +499,7 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters & fmt::format("Replica {} is registered.", repl_query->replica_name_)); return callback; } + case ReplicationQuery::Action::DROP_REPLICA: { const auto &name = repl_query->replica_name_; callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, name]() mutable { @@ -496,8 +510,9 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters & fmt::format("Replica {} is dropped.", repl_query->replica_name_)); return callback; } + case ReplicationQuery::Action::SHOW_REPLICAS: { - callback.header = {"name", "socket_address", "sync_mode", "timeout"}; + callback.header = {"name", "socket_address", "sync_mode", "timeout", "state"}; callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, replica_nfields = callback.header.size()] { const auto &replicas = handler.ShowReplicas(); auto typed_replicas = std::vector>{}; @@ -508,6 +523,7 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters & typed_replica.emplace_back(TypedValue(replica.name)); typed_replica.emplace_back(TypedValue(replica.socket_address)); + switch (replica.sync_mode) { case ReplicationQuery::SyncMode::SYNC: typed_replica.emplace_back(TypedValue("sync")); @@ -516,12 +532,28 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters & typed_replica.emplace_back(TypedValue("async")); break; } + if (replica.timeout) { typed_replica.emplace_back(TypedValue(*replica.timeout)); } else { typed_replica.emplace_back(TypedValue()); } + switch (replica.state) { + case ReplicationQuery::ReplicaState::READY: + typed_replica.emplace_back(TypedValue("ready")); + break; + case ReplicationQuery::ReplicaState::REPLICATING: + typed_replica.emplace_back(TypedValue("replicating")); + break; + case ReplicationQuery::ReplicaState::RECOVERY: + typed_replica.emplace_back(TypedValue("recovery")); + break; + case ReplicationQuery::ReplicaState::INVALID: + typed_replica.emplace_back(TypedValue("invalid")); + break; + } + typed_replicas.emplace_back(std::move(typed_replica)); } return typed_replicas; diff --git a/src/query/interpreter.hpp b/src/query/interpreter.hpp index ef790d738..78713be47 100644 --- a/src/query/interpreter.hpp +++ b/src/query/interpreter.hpp @@ -127,6 +127,7 @@ class ReplicationQueryHandler { std::string socket_address; ReplicationQuery::SyncMode sync_mode; std::optional timeout; + ReplicationQuery::ReplicaState state; }; /// @throw QueryRuntimeException if an error ocurred. diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp index c0842dbee..f58660142 100644 --- a/src/storage/v2/storage.cpp +++ b/src/storage/v2/storage.cpp @@ -306,6 +306,13 @@ Storage::Storage(Config config) uuid_(utils::GenerateUUID()), epoch_id_(utils::GenerateUUID()), global_locker_(file_retainer_.AddLocker()) { + if (config_.durability.snapshot_wal_mode == Config::Durability::SnapshotWalMode::DISABLED && + replication_role_ == ReplicationRole::MAIN) { + spdlog::warn( + "The instance has the MAIN replication role, but durability logs and snapshots are disabled. Please consider " + "enabling durability by using --storage-snapshot-interval-sec and --storage-wal-enabled flags because " + "without write-ahead logs this instance is not replicating any data."); + } if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::DISABLED || config_.durability.snapshot_on_exit || config_.durability.recover_on_startup) { // Create the directory initially to crash the database in case of diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 611165ac7..1bb23f176 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -1,25 +1,32 @@ # Set up C++ functions for e2e tests function(add_query_module target_name src) - add_library(${target_name} SHARED ${src}) - SET_TARGET_PROPERTIES(${target_name} PROPERTIES PREFIX "") - target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/include) + add_library(${target_name} SHARED ${src}) + SET_TARGET_PROPERTIES(${target_name} PROPERTIES PREFIX "") + target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/include) endfunction() - function(copy_e2e_python_files TARGET_PREFIX FILE_NAME) -add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL - COMMAND ${CMAKE_COMMAND} -E copy + add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME} ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) +endfunction() + +function(copy_e2e_python_files_from_parent_folder TARGET_PREFIX EXTRA_PATH FILE_NAME) + add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/${EXTRA_PATH}/${FILE_NAME} + ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${EXTRA_PATH}/${FILE_NAME}) endfunction() function(copy_e2e_cpp_files TARGET_PREFIX FILE_NAME) -add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL - COMMAND ${CMAKE_COMMAND} -E copy + add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME} ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) endfunction() add_subdirectory(server) diff --git a/tests/e2e/interactive_mg_runner.py b/tests/e2e/interactive_mg_runner.py new file mode 100644 index 000000000..29f496713 --- /dev/null +++ b/tests/e2e/interactive_mg_runner.py @@ -0,0 +1,219 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +# TODO(gitbuda): Add action to print the context/cluster. +# TODO(gitbuda): Add action to print logs of each Memgraph instance. +# TODO(gitbuda): Polish naming within script. +# TODO(gitbuda): Consider moving this somewhere higher in the project or even put inside GQLAlchmey. + +# The idea here is to implement simple interactive runner of Memgraph instances because: +# * it should be possible to manually create new test cases first +# by just running this script and executing command manually from e.g. mgconsole, +# running single instance of Memgraph is easy but running multiple instances and +# controlling them is not that easy +# * it should be easy to create new operational test without huge knowledge overhead +# by e.g. calling `process_actions` from any e2e Python test, the test will contain the +# string with all actions and should run test code in a different thread. +# +# NOTE: The intention here is not to provide infrastructure to write data +# correctness tests or any heavy workload, the intention is to being able to +# easily test e2e "operational" cases, simple cluster setup and basic Memgraph +# operational queries. For any type of data correctness tests Jepsen or similar +# approaches have to be employed. +# NOTE: The instance description / context should be compatible with tests/e2e/runner.py + +import atexit +import logging +import os +import subprocess +from argparse import ArgumentParser +from pathlib import Path +import time +import sys +from inspect import signature + +import yaml +from memgraph import MemgraphInstanceRunner + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..")) +BUILD_DIR = os.path.join(PROJECT_DIR, "build") +MEMGRAPH_BINARY = os.path.join(BUILD_DIR, "memgraph") + +# Cluster description, injectable as the context. +# If the script argument is not provided, the following will be used as a default. +MEMGRAPH_INSTANCES_DESCRIPTION = { + "replica1": { + "args": ["--bolt-port", "7688", "--log-level=TRACE"], + "log_file": "replica1.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"], + }, + "replica2": { + "args": ["--bolt-port", "7689", "--log-level=TRACE"], + "log_file": "replica2.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"], + }, + "main": { + "args": ["--bolt-port", "7687", "--log-level=TRACE"], + "log_file": "main.log", + "setup_queries": [ + "REGISTER REPLICA replica1 SYNC TO '127.0.0.1:10001'", + "REGISTER REPLICA replica2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'", + ], + }, +} +MEMGRAPH_INSTANCES = {} +ACTIONS = { + "info": lambda context: info(context), + "stop": lambda context, name: stop(context, name), + "start": lambda context, name: start(context, name), + "sleep": lambda context, delta: time.sleep(float(delta)), + "exit": lambda context: sys.exit(1), + "quit": lambda context: sys.exit(1), +} + +log = logging.getLogger("memgraph.tests.e2e") + + +def load_args(): + parser = ArgumentParser() + parser.add_argument("--actions", required=False, help="What actions to run", default="") + parser.add_argument( + "--context-yaml", + required=False, + help="YAML file with the cluster description", + default="", + ) + return parser.parse_args() + + +def _start_instance(name, args, log_file, queries, use_ssl, procdir): + mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl) + MEMGRAPH_INSTANCES[name] = mg_instance + log_file_path = os.path.join(BUILD_DIR, "logs", log_file) + binary_args = args + ["--log-file", log_file_path] + + if len(procdir) != 0: + binary_args.append("--query-modules-directory=" + procdir) + + mg_instance.start(args=binary_args) + for query in queries: + mg_instance.query(query) + + return mg_instance + + +def stop_all(): + for mg_instance in MEMGRAPH_INSTANCES.values(): + mg_instance.stop() + + +def stop_instance(context, name): + for key, _ in context.items(): + if key != name: + continue + MEMGRAPH_INSTANCES[name].stop() + + +def stop(context, name): + if name != "all": + stop_instance(context, name) + return + + stop_all() + + +@atexit.register +def cleanup(): + stop_all() + + +def start_instance(context, name, procdir): + mg_instances = {} + + for key, value in context.items(): + if key != name: + continue + args = value["args"] + log_file = value["log_file"] + queries = [] + if "setup_queries" in value: + queries = value["setup_queries"] + use_ssl = False + if "ssl" in value: + use_ssl = bool(value["ssl"]) + value.pop("ssl") + + instance = _start_instance(name, args, log_file, queries, use_ssl, procdir) + mg_instances[name] = instance + + assert len(mg_instances) == 1 + + return mg_instances + + +def start_all(context, procdir=""): + mg_instances = {} + for key, _ in context.items(): + mg_instances.update(start_instance(context, key, procdir)) + + return mg_instances + + +def start(context, name, procdir=""): + if name != "all": + return start_instance(context, name, procdir) + + return start_all(context) + + +def info(context): + print("{:<15s}{:>6s}".format("NAME", "STATUS")) + for name, _ in context.items(): + if name not in MEMGRAPH_INSTANCES: + continue + instance = MEMGRAPH_INSTANCES[name] + print("{:<15s}{:>6s}".format(name, "UP" if instance.is_running() else "DOWN")) + + +def process_actions(context, actions): + actions = actions.split(" ") + actions.reverse() + while len(actions) > 0: + name = actions.pop() + action = ACTIONS[name] + args_no = len(signature(action).parameters) - 1 + assert ( + args_no >= 0 + ), "Wrong action definition, each action has to accept at least 1 argument which is the context." + assert args_no <= 1, "Actions with more than one user argument are not yet supported" + if args_no == 0: + action(context) + if args_no == 1: + action(context, actions.pop()) + + +if __name__ == "__main__": + args = load_args() + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(name)s] %(message)s") + + if args.context_yaml == "": + context = MEMGRAPH_INSTANCES_DESCRIPTION + else: + with open(args.context_yaml, "r") as f: + context = yaml.load(f, Loader=yaml.FullLoader) + if args.actions != "": + process_actions(context, args.actions) + sys.exit(0) + + while True: + choice = input("ACTION>") + process_actions(context, choice) diff --git a/tests/e2e/memgraph.py b/tests/e2e/memgraph.py index c748ad02d..fd0f8336b 100755 --- a/tests/e2e/memgraph.py +++ b/tests/e2e/memgraph.py @@ -112,4 +112,4 @@ class MemgraphInstanceRunner: return self.proc_mg.kill() code = self.proc_mg.wait() - assert code == 9, "The killed Memgraph process exited with non-nine!" + assert code == -9, "The killed Memgraph process exited with non-nine!" diff --git a/tests/e2e/replication/CMakeLists.txt b/tests/e2e/replication/CMakeLists.txt index 9e462c4d7..c2b98eb6c 100644 --- a/tests/e2e/replication/CMakeLists.txt +++ b/tests/e2e/replication/CMakeLists.txt @@ -9,3 +9,6 @@ target_link_libraries(memgraph__e2e__replication__read_write_benchmark gflags js copy_e2e_python_files(replication_show common.py) copy_e2e_python_files(replication_show conftest.py) copy_e2e_python_files(replication_show show.py) +copy_e2e_python_files(replication_show show_while_creating_invalid_state.py) +copy_e2e_python_files_from_parent_folder(replication_show ".." memgraph.py) +copy_e2e_python_files_from_parent_folder(replication_show ".." interactive_mg_runner.py) diff --git a/tests/e2e/replication/show.py b/tests/e2e/replication/show.py index ed7f0268b..9d8a92af4 100755 --- a/tests/e2e/replication/show.py +++ b/tests/e2e/replication/show.py @@ -12,6 +12,7 @@ import sys import pytest + from common import execute_and_fetch_all @@ -26,21 +27,5 @@ def test_show_replication_role(port, role, connection): assert data[0][0] == role -def test_show_replicas(connection): - cursor = connection(7687, "main").cursor() - actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;")) - - expected_column_names = {"name", "socket_address", "sync_mode", "timeout"} - actual_column_names = {x.name for x in cursor.description} - assert expected_column_names == actual_column_names - - expected_data = { - ("replica_1", "127.0.0.1:10001", "sync", 0), - ("replica_2", "127.0.0.1:10002", "sync", 1.0), - ("replica_3", "127.0.0.1:10003", "async", None), - } - assert expected_data == actual_data - - if __name__ == "__main__": sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/replication/show_while_creating_invalid_state.py b/tests/e2e/replication/show_while_creating_invalid_state.py new file mode 100644 index 000000000..73d451cc4 --- /dev/null +++ b/tests/e2e/replication/show_while_creating_invalid_state.py @@ -0,0 +1,121 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import sys + +import atexit +import os +import pytest +import time + +from common import execute_and_fetch_all +import interactive_mg_runner + +interactive_mg_runner.SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +interactive_mg_runner.PROJECT_DIR = os.path.normpath( + os.path.join(interactive_mg_runner.SCRIPT_DIR, "..", "..", "..", "..") +) +interactive_mg_runner.BUILD_DIR = os.path.normpath(os.path.join(interactive_mg_runner.PROJECT_DIR, "build")) +interactive_mg_runner.MEMGRAPH_BINARY = os.path.normpath(os.path.join(interactive_mg_runner.BUILD_DIR, "memgraph")) + +MEMGRAPH_INSTANCES_DESCRIPTION = { + "replica_1": { + "args": ["--bolt-port", "7688", "--log-level=TRACE"], + "log_file": "replica1.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"], + }, + "replica_2": { + "args": ["--bolt-port", "7689", "--log-level=TRACE"], + "log_file": "replica2.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"], + }, + "replica_3": { + "args": ["--bolt-port", "7690", "--log-level=TRACE"], + "log_file": "replica3.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10003;"], + }, + "replica_4": { + "args": ["--bolt-port", "7691", "--log-level=TRACE"], + "log_file": "replica4.log", + "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10004;"], + }, + "main": { + "args": ["--bolt-port", "7687", "--log-level=TRACE"], + "log_file": "main.log", + "setup_queries": [ + "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';", + "REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002';", + "REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';", + "REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';", + ], + }, +} + + +def test_show_replicas(connection): + # Goal of this test is to check the SHOW REPLICAS command. + # 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes. + # 1/ We check that all replicas have the correct state: they should all be ready. + # 2/ We drop one replica. It should not appear anymore in the SHOW REPLICAS command. + # 3/ We kill another replica. It should become invalid in the SHOW REPLICAS command. + + # 0/ + + atexit.register( + interactive_mg_runner.stop_all + ) # Needed in case the test fails due to an assert. One still want the instances to be stoped. + mg_instances = interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION) + + cursor = connection(7687, "main").cursor() + + # 1/ + actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;")) + EXPECTED_COLUMN_NAMES = {"name", "socket_address", "sync_mode", "timeout", "state"} + + actual_column_names = {x.name for x in cursor.description} + assert EXPECTED_COLUMN_NAMES == actual_column_names + + expected_data = { + ("replica_1", "127.0.0.1:10001", "sync", 0, "ready"), + ("replica_2", "127.0.0.1:10002", "sync", 1.0, "ready"), + ("replica_3", "127.0.0.1:10003", "async", None, "ready"), + ("replica_4", "127.0.0.1:10004", "async", None, "ready"), + } + assert expected_data == actual_data + + # 2/ + execute_and_fetch_all(cursor, "DROP REPLICA replica_2") + actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;")) + expected_data = { + ("replica_1", "127.0.0.1:10001", "sync", 0, "ready"), + ("replica_3", "127.0.0.1:10003", "async", None, "ready"), + ("replica_4", "127.0.0.1:10004", "async", None, "ready"), + } + assert expected_data == actual_data + + # 3/ + mg_instances["replica_1"].kill() + mg_instances["replica_3"].kill() + mg_instances["replica_4"].stop() + + # We leave some time for the main to realise the replicas are down. + time.sleep(2) + actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;")) + expected_data = { + ("replica_1", "127.0.0.1:10001", "sync", 0, "invalid"), + ("replica_3", "127.0.0.1:10003", "async", None, "invalid"), + ("replica_4", "127.0.0.1:10004", "async", None, "invalid"), + } + assert expected_data == actual_data + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/replication/workloads.yaml b/tests/e2e/replication/workloads.yaml index da3944539..1aae85047 100644 --- a/tests/e2e/replication/workloads.yaml +++ b/tests/e2e/replication/workloads.yaml @@ -74,3 +74,7 @@ workloads: "REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'" ] validation_queries: [] + + - name: "Show while creating invalid state" + binary: "tests/e2e/pytest_runner.sh" + args: ["replication/show_while_creating_invalid_state.py"] diff --git a/tests/e2e/runner.py b/tests/e2e/runner.py index f283a578f..3c57f918e 100755 --- a/tests/e2e/runner.py +++ b/tests/e2e/runner.py @@ -18,12 +18,11 @@ from pathlib import Path import yaml -from memgraph import MemgraphInstanceRunner +import interactive_mg_runner SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..")) BUILD_DIR = os.path.join(PROJECT_DIR, "build") -MEMGRAPH_BINARY = os.path.join(BUILD_DIR, "memgraph") log = logging.getLogger("memgraph.tests.e2e") @@ -58,30 +57,22 @@ def run(args): for mg_instance in mg_instances.values(): mg_instance.stop() - for name, config in workload["cluster"].items(): - use_ssl = False - if "ssl" in config: - use_ssl = bool(config["ssl"]) - config.pop("ssl") - mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl) - mg_instances[name] = mg_instance - log_file_path = os.path.join(BUILD_DIR, "logs", config["log_file"]) - binary_args = config["args"] + ["--log-file", log_file_path] + if "cluster" in workload: + procdir = "" if "proc" in workload: - procdir = "--query-modules-directory=" + os.path.join(BUILD_DIR, workload["proc"]) - binary_args.append(procdir) - mg_instance.start(args=binary_args) - for query in config.get("setup_queries", []): - mg_instance.query(query) + procdir = os.path.join(BUILD_DIR, workload["proc"]) + mg_instances = interactive_mg_runner.start_all(workload["cluster"], procdir) + # Test. mg_test_binary = os.path.join(BUILD_DIR, workload["binary"]) subprocess.run([mg_test_binary] + workload["args"], check=True, stderr=subprocess.STDOUT) # Validation. - for name, config in workload["cluster"].items(): - for validation in config.get("validation_queries", []): - mg_instance = mg_instances[name] - data = mg_instance.query(validation["query"])[0][0] - assert data == validation["expected"] + if "cluster" in workload: + for name, config in workload["cluster"].items(): + for validation in config.get("validation_queries", []): + mg_instance = mg_instances[name] + data = mg_instance.query(validation["query"])[0][0] + assert data == validation["expected"] cleanup() log.info("%s PASSED.", workload_name)