# Copyright (c) 2023 Egor Tensin # This file is part of the "cimple" project. # For details, see https://github.com/egor-tensin/cimple. # Distributed under the MIT License. from collections import namedtuple import logging import os from pytest import fixture from lib import test_repo as repo from lib.db import Database from lib.net import random_unused_port from lib.process import CmdLine class Param: def __init__(self, codename, help_string, required=True): self.codename = codename self.help_string = help_string self.required = required @property def cmd_line(self): return f"--{self.codename.replace('_', '-')}" def add_to_parser(self, parser): parser.addoption(self.cmd_line, required=self.required, help=self.help_string) PARAMS = [ Param(f'{name}', f'cimple-{name} binary path') for name in ('server', 'worker', 'client') ] PARAMS += [ Param('sigsegv', 'sigsegv binary path'), Param('project_version', 'project version'), Param('valgrind', 'path to valgrind.sh', required=False), Param('flamegraph', 'path to flamegraph.sh', required=False), Param('flame_graphs_dir', 'directory to store flame graphs', required=False), ] def pytest_addoption(parser): for opt in PARAMS: opt.add_to_parser(parser) class Params: def __init__(self, pytestconfig): for opt in PARAMS: setattr(self, opt.codename, None) for opt in PARAMS: path = pytestconfig.getoption(opt.codename) if path is None: continue logging.info("'%s' parameter value: %s", opt.codename, path) setattr(self, opt.codename, path) @fixture(scope='session') def params(pytestconfig): return Params(pytestconfig) class CmdLineValgrind(CmdLine): def __init__(self, binary): # Signal to Valgrind that ci scripts should obviously be exempt from # memory leak checking: super().__init__(binary, '--trace-children-skip=*/ci', '--') @fixture(scope='session') def base_cmd_line(params): cmd_line = CmdLine.unbuffered() valgrind = params.valgrind if valgrind is not None: cmd_line = CmdLine.wrap(CmdLineValgrind(valgrind), cmd_line) return cmd_line @fixture(scope='session') def version(params): return params.project_version @fixture(scope='session') def server_port(): return str(random_unused_port()) @fixture def sqlite_path(tmp_path): return os.path.join(tmp_path, 'cimple.sqlite') @fixture def sqlite_db(server, sqlite_path): return Database(sqlite_path) class CmdLineServer(CmdLine): def log_line_means_process_ready(self, line): return line.endswith('Waiting for new connections') class CmdLineWorker(CmdLine): def log_line_means_process_ready(self, line): return line.endswith('Waiting for a new command') @fixture def server_exe(params): return CmdLineServer(params.server) @fixture def worker_exe(params): return CmdLineWorker(params.worker) @fixture def client_exe(params): return CmdLine(params.client) @fixture def server_cmd(base_cmd_line, params, server_port, sqlite_path): args = ['--port', server_port, '--sqlite', sqlite_path] return CmdLineServer.wrap(base_cmd_line, CmdLine(params.server, *args)) @fixture def worker_cmd(base_cmd_line, params, server_port): args = ['--host', '127.0.0.1', '--port', server_port] return CmdLineWorker.wrap(base_cmd_line, CmdLine(params.worker, *args)) @fixture def client(base_cmd_line, params, server_port): args = ['--host', '127.0.0.1', '--port', server_port] return CmdLine.wrap(base_cmd_line, CmdLine(params.client, *args)) @fixture def sigsegv(params): return CmdLine(params.sigsegv) @fixture def server(server_cmd): with server_cmd.run_async() as server: yield server assert server.returncode == 0 @fixture def workers(worker_cmd): with worker_cmd.run_async() as worker1, \ worker_cmd.run_async() as worker2: yield [worker1, worker2] assert worker1.returncode == 0 assert worker2.returncode == 0 @fixture def flame_graph_svg(params, tmp_path, flame_graph_repo): dir = params.flame_graphs_dir if dir is None: return os.path.join(tmp_path, 'flame_graph.svg') os.makedirs(dir, exist_ok=True) return os.path.join(dir, f'flame_graph_{flame_graph_repo.codename()}.svg') @fixture def profiler(params, server, workers, flame_graph_svg): pids = [server.pid] + [worker.pid for worker in workers] pids = map(str, pids) cmd_line = CmdLine(params.flamegraph, flame_graph_svg, *pids) with cmd_line.run_async() as proc: yield assert proc.returncode == 0 @fixture def repo_path(tmp_path): return os.path.join(tmp_path, 'repo') TEST_REPOS = [ repo.TestRepoOutputSimple, repo.TestRepoOutputEmpty, repo.TestRepoOutputLong, repo.TestRepoOutputNull, repo.TestRepoSegfault, ] STRESS_TEST_REPOS = [ repo.TestRepoOutputSimple, repo.TestRepoOutputLong, ] def _make_repo(repo_path, params, cls): args = [repo_path] if cls is repo.TestRepoSegfault: args += [params.sigsegv] return cls(*args) @fixture(params=TEST_REPOS, ids=[repo.codename() for repo in TEST_REPOS]) def test_repo(repo_path, params, request): return _make_repo(repo_path, params, request.param) @fixture(params=STRESS_TEST_REPOS, ids=[repo.codename() for repo in STRESS_TEST_REPOS]) def stress_test_repo(repo_path, params, request): return _make_repo(repo_path, params, request.param) @fixture def flame_graph_repo(stress_test_repo): return stress_test_repo Env = namedtuple('Env', ['server', 'workers', 'client', 'db']) @fixture def env(server, workers, client, sqlite_db): return Env(server, workers, client, sqlite_db)