diff --git a/tests/__init__.py b/tests/__init__.py index 70659a9c..ca188fa6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,6 +9,7 @@ import os import re import six import time +import yaml from traceback import extract_stack import koji @@ -55,6 +56,13 @@ def read_staged_data(yaml_name): return to_text_type(mmd.read()) +def read_staged_data_as_yaml(yaml_name): + filename = staged_data_filename( + yaml_name if '.' in yaml_name else "{}.yaml".format(yaml_name)) + with open(filename, "r") as f: + return yaml.safe_load(f) + + def patch_config(): # add test builders for all resolvers with_test_builders = dict() diff --git a/tests/test_memory/__init__.py b/tests/test_memory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_memory/mbs_configuration.py b/tests/test_memory/mbs_configuration.py new file mode 100644 index 00000000..8f0e0430 --- /dev/null +++ b/tests/test_memory/mbs_configuration.py @@ -0,0 +1,32 @@ +import os + + +class TestConfiguration: + # TEST CONFIGURATION ('borrowed' from module_build_service.common.conf) + SECRET_KEY = os.urandom(16) + SQLALCHEMY_TRACK_MODIFICATIONS = True + HOST = "0.0.0.0" + PORT = 5000 + + LOG_LEVEL = "debug" + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI", "sqlite:///:memory:") + DEBUG = True + MESSAGING = "in_memory" + PDC_URL = "https://pdc.fedoraproject.org/rest_api/v1" + NET_TIMEOUT = 10 + NET_RETRY_INTERVAL = 1 + SCM_NET_TIMEOUT = 0.1 + SCM_NET_RETRY_INTERVAL = 0.1 + KOJI_CONFIG = "./conf/koji.conf" + KOJI_PROFILE = "staging" + KOJI_REPOSITORY_URL = "https://kojipkgs.stg.fedoraproject.org/repos" + SCMURLS = ["https://src.stg.fedoraproject.org/modules/"] + ALLOWED_GROUPS_TO_IMPORT_MODULE = {"mbs-import-module"} + GREENWAVE_URL = "https://greenwave.example.local/api/v1.0/" + GREENWAVE_DECISION_CONTEXT = "test_dec_context" + GREENWAVE_SUBJECT_TYPE = "some-module" + STREAM_SUFFIXES = {r"^el\d+\.\d+\.\d+\.z$": 0.1} + CELERY_TASK_ALWAYS_EAGER = True + + NO_AUTH = True + YAML_SUBMIT_ALLOWED = True diff --git a/tests/test_memory/mbs_debug.py b/tests/test_memory/mbs_debug.py new file mode 100644 index 00000000..a02f3d8e --- /dev/null +++ b/tests/test_memory/mbs_debug.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import, print_function + +import koji +import mock +import signal +import threading +import types + +from module_build_service import app +from module_build_service.common import conf +from module_build_service.builder import GenericBuilder + +from tests.test_build.test_build import FakeModuleBuilder +from tests.test_build.test_build import main as run_scheduler + + +def patch_config_system_setter(): + """bypass supported builders check""" + def set_system(self, system): + self._system = system + conf._setifok_system = types.MethodType(set_system, conf) + + +def register_fake_builder(): + patch_config_system_setter() + conf.system = FakeModuleBuilder.backend + GenericBuilder.register_backend_class(FakeModuleBuilder) + + # Builder always instantly succeeds + def on_get_task_info_cb(cls, task_id): + return {"state": koji.TASK_STATES["CLOSED"]} + FakeModuleBuilder.on_get_task_info_cb = on_get_task_info_cb + + +class SimpleMock: + """Dummy callable mock object - we want our memory footprint to be as small as possible""" + def __call__(self, *args, **kwargs): + return True + + +@mock.patch("module_build_service.scheduler.handlers.modules.handle_stream_collision_modules", + new_callable=SimpleMock) +@mock.patch("module_build_service.scheduler.handlers.modules.record_module_build_arches", + new_callable=SimpleMock) +@mock.patch("module_build_service.scheduler.greenwave.Greenwave.check_gating", + new_callable=SimpleMock) +def run_debug_instance(mock_1, mock_2, mock_3, host=None, port=None): + + def handle_pdb(sig, frame): + import pdb + pdb.Pdb().set_trace(frame) + + # kill -10 to start debugger + signal.signal(signal.SIGUSR1, handle_pdb) + + register_fake_builder() + + host = host or conf.host + port = port or conf.port + + def run_app(): + app.run(host=host, port=port, debug=False) + threading.Thread(target=run_app, daemon=True).start() + + # run moksha hub and never stop + run_scheduler([], stop_condition=lambda msg: False) + + +if __name__ == '__main__': + run_debug_instance() diff --git a/tests/test_memory/test_memory.py b/tests/test_memory/test_memory.py new file mode 100644 index 00000000..690f6b93 --- /dev/null +++ b/tests/test_memory/test_memory.py @@ -0,0 +1,103 @@ +import json +import os +import psutil +import pytest +import requests +import time + +from subprocess import Popen +from tests import read_staged_data_as_yaml + + +base_dir = os.path.dirname(__file__) +yaml = read_staged_data_as_yaml("testmodule.yaml") + +LOCAL_MBS_URL = "http://localhost:5000/module-build-service/1/module-builds/" + + +def submit_yaml_build(module_name): + """Submit module build with custom name to get a unique NVR each time.""" + yaml["data"]["name"] = module_name + data = {"modulemd": str(yaml), "module_name": "testmodule"} + r = requests.post(LOCAL_MBS_URL, data=json.dumps(data)) + if r.status_code > 300: + pytest.fail(str(r.json())) + return r.json()["id"] + + +def wait_for_module_build(build_id, timeout=60, interval=5): + """Wait for module build to be ready + + :param int build_id: build definition (either id or Build object) + :param float timeout: timeout in seconds + :param float interval: scan interval in seconds + """ + start = time.time() + + while (time.time() - start) < timeout: + state = requests.get(LOCAL_MBS_URL + str(build_id)).json()["state_name"] + if state == "ready": + return + time.sleep(interval) + pytest.skip("Wait for build timed out after {}s".format(timeout)) + + +@pytest.fixture() +def run_debug_mbs_instance(): + """Starts a 'debug' MBS instance: + * mbs-frontend (no auth, yaml import enabled) + * moksha hub (in memory messaging) + * tests.test_build.test_build.FakeModuleBuilder as builder backend (always succeeds) + + Optionally: + Set MBS_TEST_INSTANCE_PID env variable to run test against your own running instance. + If you intend to run a standalone instance, make sure you have these env vars set: + * MODULE_BUILD_SERVICE_DEVELOPER_ENV=0 + * MBS_CONFIG_SECTION=TestConfiguration + * MBS_CONFIG_FILE=tests/test_memory/mbs_configuration.py + * DATABASE_URI=postgresql+psycopg2://postgres:@127.0.0.1/mbstest + + ...then 'python tests/test_memory/mbs_debug.py' (and 'kill -10 ' to start debugger) + """ + + process = None + try: + running_instance_pid = int(os.environ.get("MBS_TEST_INSTANCE_PID")) + yield running_instance_pid + except TypeError or ValueError: + mbs_config_file_path = os.path.join(base_dir, "mbs_configuration.py") + env = { + "MBS_CONFIG_SECTION": "TestConfiguration", + "MBS_CONFIG_FILE": mbs_config_file_path, + } + # Pass the preset database configuration (if present) + if os.environ.get("DATABASE_URI"): + env["DATABASE_URI"] = os.environ.get("DATABASE_URI") + + mbs_exec_script = os.path.join(base_dir, "mbs_debug.py") + process = Popen(["python", mbs_exec_script], stdin=None, stdout=None, env=env) + time.sleep(5) # wait a couple of secs for MBS to start + yield process.pid + if process: + process.terminate() + + +@pytest.mark.parametrize("num_builds", [20]) +def test_submit_build(require_platform_and_default_arch, run_debug_mbs_instance, num_builds): + pid = run_debug_mbs_instance + process = psutil.Process(pid) + + def get_rss(): # resident set size in MB + return process.memory_info().rss / 1000000 + consumed_memory = [] + + for i in range(num_builds): + build_id = submit_yaml_build("test-module-{}".format(i)) + # wait for the build to finish, so that the build logger is flushed/closed + wait_for_module_build(build_id, interval=0.5, timeout=10) + consumed_memory.append(get_rss()) + + print("Memory [MB]: {}".format(consumed_memory)) + + if (consumed_memory[-1] - consumed_memory[0]) > 0.1: + pytest.fail("Memory is leaking, [MB]: {}".format(consumed_memory)) diff --git a/tox.ini b/tox.ini index 7fce33a7..db649b5a 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,7 @@ deps = -r{toxinidir}/test-requirements.txt commands = py.test -v \ --ignore tests/integration \ + --ignore tests/test_memory \ --cov module_build_service \ --cov-report html \ --cov-report term \ @@ -91,3 +92,12 @@ commands = --html=report.html \ --self-contained-html \ {posargs:tests/integration} + +[testenv:memory] +basepython = python3 +deps = {[testenv]deps} +passenv = + MBS_TEST_INSTANCE_PID + DATABASE_URI +commands = + py.test -rA {posargs:tests/test_memory --show-capture=no}