aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/CMakeLists.txt11
-rw-r--r--test/__init__.py0
-rw-r--r--test/conftest.py128
-rw-r--r--test/lib/__init__.py0
-rw-r--r--test/lib/process.py104
-rw-r--r--test/pytest.ini5
-rw-r--r--test/test_basic.py16
7 files changed, 264 insertions, 0 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..5d16f4e
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,11 @@
+find_package(Python3 REQUIRED COMPONENTS Interpreter)
+
+add_test(
+ NAME integration_tests
+ COMMAND Python3::Interpreter -m pytest
+ "${CMAKE_CURRENT_SOURCE_DIR}"
+ --server-binary "$<TARGET_FILE:server>"
+ --worker-binary "$<TARGET_FILE:worker>"
+ --client-binary "$<TARGET_FILE:client>"
+ --project-version "${PROJECT_VERSION}"
+ WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000..5f70bd5
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2023 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "cimple" project.
+# For details, see https://github.com/egor-tensin/cimple.
+# Distributed under the MIT License.
+
+import logging
+import os
+import random
+
+from pytest import fixture
+
+from .lib.process import run, run_async
+
+
+class CmdLineOption:
+ def __init__(self, codename, help_string):
+ self.codename = codename
+ self.help_string = help_string
+
+ @property
+ def cmd_line(self):
+ return f"--{self.codename.replace('_', '-')}"
+
+
+class CmdLineBinary(CmdLineOption):
+ def __init__(self, name):
+ self.name = name
+ super().__init__(self.get_code_name(), self.get_help_string())
+
+ def get_code_name(self):
+ return f'{self.name}_binary'
+
+ @property
+ def basename(self):
+ return f'cimple-{self.name}'
+
+ def get_help_string(self):
+ return f'{self.basename} binary path'
+
+
+CMD_LINE_BINARIES = [CmdLineBinary(name) for name in ('server', 'worker', 'client')]
+
+
+class CmdLineVersion(CmdLineOption):
+ def __init__(self):
+ super().__init__('project_version', 'project version')
+
+
+CMD_LINE_VERSION = CmdLineVersion()
+CMD_LINE_OPTIONS = CMD_LINE_BINARIES + [CMD_LINE_VERSION]
+
+
+def pytest_addoption(parser):
+ for opt in CMD_LINE_OPTIONS:
+ parser.addoption(opt.cmd_line, required=True, help=opt.help_string)
+
+
+def pytest_generate_tests(metafunc):
+ for opt in CMD_LINE_OPTIONS:
+ if opt.codename in metafunc.fixturenames:
+ metafunc.parametrize(opt.codename, metafunc.config.getoption(opt.codename))
+
+
+@fixture(scope='session')
+def rng():
+ random.seed()
+
+
+class Paths:
+ def __init__(self, pytestconfig):
+ for binary in CMD_LINE_BINARIES:
+ path = pytestconfig.getoption(binary.codename)
+ logging.info('%s path: %s', binary.basename, path)
+ setattr(self, binary.codename, path)
+
+
+@fixture
+def paths(pytestconfig):
+ return Paths(pytestconfig)
+
+
+@fixture
+def version(pytestconfig):
+ return pytestconfig.getoption(CMD_LINE_VERSION.codename)
+
+
+@fixture
+def server_port(rng):
+ return str(random.randint(2000, 50000))
+
+
+@fixture
+def sqlite_path(tmp_path):
+ return os.path.join(tmp_path, 'cimple.sqlite')
+
+
+@fixture
+def server(paths, server_port, sqlite_path):
+ with run_async(paths.server_binary, '--port', server_port, '--sqlite', sqlite_path) as server:
+ yield
+ assert server.returncode == 0
+
+
+@fixture
+def workers(paths, server_port):
+ args = [paths.worker_binary, '--host', '127.0.0.1', '--port', server_port]
+ with run_async(*args) as worker1, run_async(*args) as worker2:
+ yield
+ assert worker1.returncode == 0
+ assert worker2.returncode == 0
+
+
+@fixture
+def server_and_workers(server, workers):
+ yield
+
+
+class Client:
+ def __init__(self, binary):
+ self.binary = binary
+
+ def run(self, *args):
+ return run(self.binary, *args)
+
+
+@fixture
+def client(paths):
+ return Client(paths.client_binary)
diff --git a/test/lib/__init__.py b/test/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/lib/__init__.py
diff --git a/test/lib/process.py b/test/lib/process.py
new file mode 100644
index 0000000..f757167
--- /dev/null
+++ b/test/lib/process.py
@@ -0,0 +1,104 @@
+# Copyright (c) 2023 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "cimple" project.
+# For details, see https://github.com/egor-tensin/cimple.
+# Distributed under the MIT License.
+
+from contextlib import contextmanager
+import logging
+import os
+import subprocess
+from threading import Thread
+import time
+
+
+_COMMON_ARGS = {
+ 'text': True,
+ 'stdin': subprocess.DEVNULL,
+ 'stdout': subprocess.PIPE,
+ 'stderr': subprocess.STDOUT,
+}
+
+
+def _canonicalize_process_args(binary, *args):
+ binary = os.path.abspath(binary)
+ argv = list(args)
+ argv = [binary] + argv
+ return binary, argv
+
+
+def _log_process_args(binary, argv):
+ if argv:
+ logging.info('Executing binary %s with arguments: %s', binary, ' '.join(argv[1:]))
+ else:
+ logging.info('Executing binary %s', binary)
+
+
+class LoggingThread(Thread):
+ def __init__(self, process):
+ self.process = process
+ target = lambda pipe: self.consume(pipe)
+ super().__init__(target=target, args=[process.stdout])
+
+ def consume(self, pipe):
+ for line in iter(pipe):
+ logging.info('%s: %s', self.process.log_id, line)
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, *args):
+ self.process.shut_down()
+ self.join()
+
+
+class Process(subprocess.Popen):
+ def __init__(self, binary, *args):
+ binary, argv = _canonicalize_process_args(binary, *args)
+ _log_process_args(binary, argv)
+
+ self.binary = binary
+ self.name = os.path.basename(binary)
+
+ super().__init__(argv, **_COMMON_ARGS)
+ # TODO: figure out how to remove this.
+ time.sleep(1)
+ logging.info('Process %s launched', self.log_id)
+
+ @property
+ def log_id(self):
+ return f'{self.pid}/{self.name}'
+
+ def __exit__(self, *args):
+ try:
+ self.shut_down()
+ finally:
+ super().__exit__(*args)
+
+ def shut_down(self):
+ ec = self.poll()
+ if ec is not None:
+ return
+ logging.info('Terminating process %s', self.log_id)
+ self.terminate()
+ try:
+ self.wait(timeout=3)
+ return
+ except subprocess.TimeoutExpired:
+ pass
+ self.kill()
+ self.wait(timeout=3)
+
+
+@contextmanager
+def run_async(binary, *args):
+ with Process(binary, *args) as process, \
+ LoggingThread(process):
+ yield process
+
+
+def run(binary, *args):
+ binary, argv = _canonicalize_process_args(binary, *args)
+ _log_process_args(binary, argv)
+ result = subprocess.run(argv, **_COMMON_ARGS)
+ return result.returncode, result.stdout
diff --git a/test/pytest.ini b/test/pytest.ini
new file mode 100644
index 0000000..cdc0831
--- /dev/null
+++ b/test/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+log_format = %(asctime)s | %(levelname)s | %(message)s
+log_date_format = %Y-%m-%d %H:%M:%S
+log_cli_level = INFO
+#log_cli = 1
diff --git a/test/test_basic.py b/test/test_basic.py
new file mode 100644
index 0000000..b072384
--- /dev/null
+++ b/test/test_basic.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2023 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "cimple" project.
+# For details, see https://github.com/egor-tensin/cimple.
+# Distributed under the MIT License.
+
+import pytest
+
+
+def test_server_and_workers_run(server_and_workers):
+ pass
+
+
+def test_client_version(client, version):
+ ec, output = client.run('--version')
+ assert ec == 0
+ assert output.endswith(version + '\n')