memgraph/tests/gql_behave/continuous_integration
Aidar Samerkhanov 953a8f5340
Add handling of deleted return values for query procedures and functions ran in analytical mode (#1395)
Co-authored-by: Ante Pušić <ante.pusic@memgraph.io>
2023-12-04 08:32:59 +01:00

220 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Continuous integration toolkit. The purpose of this script is to generate
everything which is needed for the CI environment.
List of responsibilities:
* execute default suites
* terminate execution if any of internal scenarios fails
* creates the report file that is needed by the Apollo plugin
to post the status on Phabricator. (.quality_assurance_status)
"""
import argparse
import atexit
import copy
import json
import os
import subprocess
import sys
import tempfile
import time
import yaml
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
TESTS_DIR = os.path.join(SCRIPT_DIR, "tests")
BASE_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
BUILD_DIR = os.path.join(BASE_DIR, "build")
def wait_for_server(port, delay=0.01):
cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
count = 0
while subprocess.call(cmd) != 0:
time.sleep(0.01)
if count > 20 / 0.01:
print("Could not wait for server on port", port, "to startup!")
sys.exit(1)
count += 1
time.sleep(delay)
def generate_result_csv(suite, result_path):
if not os.path.exists(result_path):
return ""
with open(result_path) as f:
result = json.load(f)
ret = ""
for i in ["total", "passed"]:
ret += "{},{},{}\n".format(suite, i, result[i])
return ret
def generate_status(suite, result_path, required):
if not os.path.exists(result_path):
return ("Internal error!", 0, 1)
with open(result_path) as f:
result = json.load(f)
total = result["total"]
passed = result["passed"]
ratio = passed / total
msg = "{} / {} ({:.2%})".format(passed, total, ratio)
if required:
if passed == total:
msg += " &#x1F44D;" # +1 emoji
else:
msg += " &#x26D4;" # no entry emoji
return (msg, passed, total)
def generate_result_html(data):
ret = "<!DOCTYPE html>\n"
ret += "<h1>Memgraph GQL Behave Tests Status</h1>\n"
ret += "<table>\n"
for row in data:
ret += " <tr>\n"
for item in row:
if row == data[0]:
fmt = " <th>{}</th>\n"
else:
fmt = " <td>{}</td>\n"
ret += fmt.format(item)
ret += " </tr>\n"
ret += "</table>\n"
return ret
class MemgraphRunner:
def __init__(self, build_directory):
self.build_directory = build_directory
self.proc_mg = None
self.args = []
def start(self, args=[]):
if args == self.args and self.is_running():
return
self.stop()
self.args = copy.deepcopy(args)
self.data_directory = tempfile.TemporaryDirectory()
memgraph_binary = os.path.join(self.build_directory, "memgraph")
args_mg = [
memgraph_binary,
"--storage-properties-on-edges=true",
"--data-directory",
self.data_directory.name,
"--log-file",
str(os.path.join(BASE_DIR, "tests", "gql_behave", "memgraph.log")),
]
self.proc_mg = subprocess.Popen(args_mg + self.args)
wait_for_server(7687, 1)
assert self.is_running(), "The Memgraph process died!"
def is_running(self):
if self.proc_mg is None:
return False
if self.proc_mg.poll() is not None:
return False
return True
def stop(self):
if not self.is_running():
return
self.proc_mg.terminate()
code = self.proc_mg.wait()
assert code == 0, "The Memgraph process exited with non-zero!"
def main():
# Parse args
argp = argparse.ArgumentParser()
argp.add_argument("--build-directory", default=BUILD_DIR)
argp.add_argument("--cluster-size", default=3, type=int)
args = argp.parse_args()
# Load tests from config file
with open(os.path.join(TESTS_DIR, "config.yaml")) as f:
suites = yaml.safe_load(f)
# venv used to run the qa engine
venv_python = os.path.join(BASE_DIR, "tests", "ve3", "bin", "python3")
# Temporary directory for suite results
output_dir = tempfile.TemporaryDirectory()
# Memgraph runner
memgraph = MemgraphRunner(args.build_directory)
@atexit.register
def cleanup():
memgraph.stop()
# Results storage
result_csv = "suite,status,quantity\n"
status_data = [["Suite", "Status"]]
mandatory_fails = []
# Run suites
old_storage_mode = ""
for suite in suites:
print("Starting suite '{}' scenarios.".format(suite["name"]))
suite_storage_mode = suite["storage_mode"]
if old_storage_mode != suite_storage_mode:
memgraph.stop()
memgraph.start(["--storage-mode", suite_storage_mode])
old_storage_mode = suite_storage_mode
suite["stats_file"] = os.path.join(output_dir.name, suite["name"] + ".json")
cmd = [
venv_python,
"-u",
os.path.join(SCRIPT_DIR, "run.py"),
"--stats-file",
suite["stats_file"],
suite["test_suite"],
]
# The exit code isn't checked here because the `behave` framework
# returns a non-zero exit code when some tests fail.
subprocess.run(cmd)
suite_status, suite_passed, suite_total = generate_status(
suite["name"], suite["stats_file"], suite["must_pass"]
)
status_data.append([suite["name"], suite_status])
result_csv += generate_result_csv(suite["name"], suite["stats_file"])
if suite["must_pass"] and suite_passed != suite_total:
mandatory_fails.append(suite["name"])
break
# Create status message
result_html = generate_result_html(status_data)
# Create the report file
result_html_path = os.path.join(SCRIPT_DIR, "gql_behave_status.html")
with open(result_html_path, "w") as f:
f.write(result_html)
# Create the measurements file
result_csv_path = os.path.join(SCRIPT_DIR, "gql_behave_status.csv")
with open(result_csv_path, "w") as f:
f.write(result_csv)
print(f"CSV status is generated in {result_csv_path}")
print(f"HTML status is generated in {result_html_path}")
# Check if tests failed
if mandatory_fails != []:
sys.exit(f"Some tests that must pass have failed: {mandatory_fails}")
if __name__ == "__main__":
main()