aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2023-05-01 17:40:33 +0200
committerEgor Tensin <Egor.Tensin@gmail.com>2023-05-06 16:24:23 +0200
commit1907a90783165d13a53f600a82cd00dbb75a10ed (patch)
tree042b999cb414d1aec9043f4b72f00e3ede84db7b
parentadd a cimple-valgrind executable (diff)
downloadcimple-1907a90783165d13a53f600a82cd00dbb75a10ed.tar.gz
cimple-1907a90783165d13a53f600a82cd00dbb75a10ed.zip
add a couple of the most basic tests using Pytest
I'm super-unsure about this; I don't really like all the magic, but we'll see, I guess?
-rw-r--r--.dockerignore1
-rw-r--r--.gitignore2
-rw-r--r--CMakeLists.txt3
-rw-r--r--Dockerfile5
-rw-r--r--Makefile4
-rw-r--r--README.md6
-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
13 files changed, 283 insertions, 2 deletions
diff --git a/.dockerignore b/.dockerignore
index 01dd835..d6441d7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,3 +4,4 @@
!/LICENSE.txt
!/Makefile
!/src/**
+!/test/**
diff --git a/.gitignore b/.gitignore
index 0e03e15..3c2febd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
/.build/
+
+*.pyc
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c82ef24..848890d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.3) # for include-what-you-use
project(cimple VERSION 0.0.1 LANGUAGES C)
+enable_testing()
+
add_subdirectory(src)
+add_subdirectory(test)
install(FILES LICENSE.txt DESTINATION "share/doc/${PROJECT_NAME}")
diff --git a/Dockerfile b/Dockerfile
index 47312bf..e0eb0e6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ ARG install_dir="/app/install"
FROM base AS builder
-RUN build_deps='bash bsd-compat-headers build-base clang cmake libgit2-dev python3 sqlite-dev' && \
+RUN build_deps='bash bsd-compat-headers build-base clang cmake libgit2-dev py3-pytest sqlite-dev' && \
apk add -q --no-cache $build_deps
ARG C_COMPILER=clang
@@ -21,7 +21,8 @@ RUN cd -- "$src_dir" && \
"C_COMPILER=$C_COMPILER" \
"BUILD_TYPE=$BUILD_TYPE" \
"DEFAULT_HOST=$DEFAULT_HOST" \
- "INSTALL_PREFIX=$install_dir"
+ "INSTALL_PREFIX=$install_dir" && \
+ make test BUILD_TYPE="$BUILD_TYPE"
FROM base
diff --git a/Makefile b/Makefile
index 5ae034d..450e57b 100644
--- a/Makefile
+++ b/Makefile
@@ -58,3 +58,7 @@ build:
.PHONY: install
install: build
cmake --install '$(call escape,$(cmake_dir))'
+
+.PHONY: test
+test:
+ cd -- '$(call escape,$(cmake_dir))' && ctest -C '$(call escape,$(BUILD_TYPE))' --verbose
diff --git a/README.md b/README.md
index 0e585bd..8052287 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,12 @@ directory:
make build
+### Testing
+
+After building, you can run the "test suite" (depends on Pytest).
+
+ make test
+
### Code style
Set up the git pre-commit hook by running `./scripts/setup-hooks.sh`.
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')