aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/test
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2023-06-28 12:56:18 +0200
committerEgor Tensin <Egor.Tensin@gmail.com>2023-06-28 19:12:54 +0200
commitc52e9a6429e9293265ed7912565ee3d1c5fd3217 (patch)
tree97afc850d7e973ddc0916ceb4f76b06ae2117351 /test
parenttest/lib: refactoring (diff)
downloadcimple-c52e9a6429e9293265ed7912565ee3d1c5fd3217.tar.gz
cimple-c52e9a6429e9293265ed7912565ee3d1c5fd3217.zip
test: add test for actual CI runs
Diffstat (limited to 'test')
-rw-r--r--test/conftest.py18
-rw-r--r--test/lib/process.py78
-rw-r--r--test/lib/test_repo.py48
-rwxr-xr-xtest/lib/test_repo/ci.sh24
-rw-r--r--test/test_basic.py35
5 files changed, 182 insertions, 21 deletions
diff --git a/test/conftest.py b/test/conftest.py
index 65d8a7e..a3c8e6a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -10,6 +10,7 @@ import random
from pytest import fixture
from .lib.process import CmdLine, CmdLineRunner, Runner
+from .lib.test_repo import TestRepo
class Param:
@@ -121,12 +122,12 @@ def sqlite_path(tmp_path):
class CmdLineServer(CmdLine):
- def log_line_means_ready(self, line):
+ def log_line_means_process_ready(self, line):
return line.endswith('Waiting for new connections')
class CmdLineWorker(CmdLine):
- def log_line_means_ready(self, line):
+ def log_line_means_process_ready(self, line):
return line.endswith('Waiting for a new command')
@@ -135,7 +136,7 @@ def server(process_runner, paths, server_port, sqlite_path):
args = ['--port', server_port, '--sqlite', sqlite_path]
cmd_line = CmdLineServer(paths.server_binary, *args)
with process_runner.run_async(cmd_line) as server:
- yield
+ yield server
assert server.returncode == 0
@@ -145,18 +146,23 @@ def workers(process_runner, paths, server_port):
cmd_line = CmdLineWorker(paths.worker_binary, *args)
with process_runner.run_async(cmd_line) as worker1, \
process_runner.run_async(cmd_line) as worker2:
- yield
+ yield [worker1, worker2]
assert worker1.returncode == 0
assert worker2.returncode == 0
@fixture
def server_and_workers(server, workers):
- yield
+ yield server, workers
@fixture
def client(process_runner, paths, server_port):
- args = ['--port', server_port]
+ args = ['--host', '127.0.0.1', '--port', server_port]
cmd_line = CmdLineRunner(process_runner, paths.client_binary, *args)
return cmd_line
+
+
+@fixture
+def test_repo(tmp_path):
+ return TestRepo(tmp_path)
diff --git a/test/lib/process.py b/test/lib/process.py
index bdfe3c3..be2dc92 100644
--- a/test/lib/process.py
+++ b/test/lib/process.py
@@ -8,7 +8,7 @@ import logging
import os
import shutil
import subprocess
-from threading import Event, Thread
+from threading import Event, Lock, Thread
_COMMON_ARGS = {
@@ -19,24 +19,55 @@ _COMMON_ARGS = {
}
+def run(*args, **kwargs):
+ try:
+ return subprocess.run(list(args), check=True, **_COMMON_ARGS, **kwargs)
+ except subprocess.CalledProcessError as e:
+ logging.error('Command %s exited with code %s', e.cmd, e.returncode)
+ logging.error('Output:\n%s', e.output)
+ raise
+
+
+class LoggingEvent(Event):
+ def __init__(self, timeout=10):
+ self.timeout = timeout
+ super().__init__()
+
+ def log_line_matches(self, line):
+ return False
+
+ def wait(self):
+ if not super().wait(self.timeout):
+ raise RuntimeError('timed out while waiting for an event')
+
+
class LoggingThread(Thread):
- def __init__(self, process):
+ def __init__(self, process, events=None):
self.process = process
- self.ready_event = Event()
-
- target = lambda pipe: self.consume(pipe)
- super().__init__(target=target, args=[process.stdout])
+ self.events_lock = Lock()
+ if events is None:
+ events = []
+ self.events = events
+ super().__init__(target=lambda: self.process_output_lines())
self.start()
- self.ready_event.wait()
- def consume(self, pipe):
- for line in pipe:
+ def add_event(self, event):
+ with self.events_lock:
+ self.events.append(event)
+
+ def process_output_lines(self):
+ for line in self.process.stdout:
line = line.removesuffix('\n')
logging.info('%s: %s', self.process.log_id, line)
- if not self.ready_event.is_set() and self.process.cmd_line.log_line_means_ready(line):
- logging.info('Process %s is ready', self.process.log_id)
- self.ready_event.set()
+ with self.events_lock:
+ for event in self.events:
+ if event.is_set():
+ continue
+ if not event.log_line_matches(line):
+ continue
+ event.set()
+ self.events = [event for event in self.events if not event.is_set()]
class CmdLine:
@@ -61,7 +92,7 @@ class CmdLine:
name = os.path.basename(binary)
self.process_name = name
- def log_line_means_ready(self, line):
+ def log_line_means_process_ready(self, line):
return True
@classmethod
@@ -75,12 +106,29 @@ class CmdLine:
logging.info('Executing binary %s', self.binary)
+class LoggingEventProcessReady(LoggingEvent):
+ def __init__(self, process):
+ self.process = process
+ super().__init__()
+
+ def set(self):
+ logging.info('Process %s is ready', self.process.log_id)
+ super().set()
+
+ def log_line_matches(self, line):
+ return self.process.cmd_line.log_line_means_process_ready(line)
+
+
class Process(subprocess.Popen):
def __init__(self, cmd_line):
self.cmd_line = cmd_line
+
super().__init__(cmd_line.argv, **_COMMON_ARGS)
logging.info('Process %s has started', self.log_id)
- self.logger = LoggingThread(self)
+
+ ready_event = LoggingEventProcessReady(self)
+ self.logger = LoggingThread(self, [ready_event])
+ ready_event.wait()
@property
def log_id(self):
@@ -135,7 +183,7 @@ class Runner:
cmd_line = self._wrap(cmd_line)
cmd_line.log_process_start()
- result = subprocess.run(cmd_line.argv, **_COMMON_ARGS)
+ result = run(*cmd_line.argv)
return result.returncode, result.stdout
@contextmanager
diff --git a/test/lib/test_repo.py b/test/lib/test_repo.py
new file mode 100644
index 0000000..f12b374
--- /dev/null
+++ b/test/lib/test_repo.py
@@ -0,0 +1,48 @@
+# 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 shutil
+
+from .process import run
+
+
+class Repo:
+ BRANCH = 'main'
+
+ def __init__(self, path):
+ self.path = os.path.abspath(path)
+ os.makedirs(path, exist_ok=True)
+ self.run('git', 'init', f'--initial-branch={Repo.BRANCH}')
+ self.run('git', 'config', 'user.name', 'Test User')
+ self.run('git', 'config', 'user.email', 'test@example.com')
+
+ def run(self, *args, **kwargs):
+ run(*args, cwd=self.path, **kwargs)
+
+
+class TestRepo(Repo):
+ # Prevent Pytest from discovering test cases in this class:
+ __test__ = False
+
+ DATA_DIR = 'test_repo'
+ CI_SCRIPT = 'ci.sh'
+ OUTPUT_DIR = 'output'
+
+ @staticmethod
+ def get_ci_script():
+ return os.path.join(os.path.dirname(__file__), TestRepo.DATA_DIR, TestRepo.CI_SCRIPT)
+
+ def __init__(self, path):
+ super().__init__(path)
+ shutil.copy(self.get_ci_script(), self.path)
+ self.run('git', 'add', '.')
+ self.run('git', 'commit', '-m', 'add CI script')
+ self.output_dir = os.path.join(self.path, TestRepo.OUTPUT_DIR)
+ os.makedirs(self.output_dir, exist_ok=True)
+
+ def count_ci_output_files(self):
+ return len([name for name in os.listdir(self.output_dir) if os.path.isfile(os.path.join(self.output_dir, name))])
diff --git a/test/lib/test_repo/ci.sh b/test/lib/test_repo/ci.sh
new file mode 100755
index 0000000..804cdb3
--- /dev/null
+++ b/test/lib/test_repo/ci.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -o errexit -o nounset -o pipefail
+shopt -s inherit_errexit lastpipe
+
+script_dir="$( dirname -- "${BASH_SOURCE[0]}" )"
+script_dir="$( cd -- "$script_dir" && pwd )"
+readonly script_dir
+
+ci_output_dir="$( git remote get-url origin )"
+ci_output_dir="$ci_output_dir/output"
+readonly ci_output_dir
+
+mkdir -p -- "$ci_output_dir"
+
+readonly ci_output_template=ci_XXXXXX
+
+ci_output_path="$( mktemp --tmpdir="$ci_output_dir" "$ci_output_template" )"
+readonly ci_output_path
+
+timestamp="$( date --iso-8601=ns )"
+readonly timestamp
+
+echo "A CI run happened at $timestamp" | tee -- "$ci_output_path"
diff --git a/test/test_basic.py b/test/test_basic.py
index b072384..7deca0f 100644
--- a/test/test_basic.py
+++ b/test/test_basic.py
@@ -5,6 +5,8 @@
import pytest
+from .lib.process import LoggingEvent
+
def test_server_and_workers_run(server_and_workers):
pass
@@ -14,3 +16,36 @@ def test_client_version(client, version):
ec, output = client.run('--version')
assert ec == 0
assert output.endswith(version + '\n')
+
+
+class LoggingEventRunComplete(LoggingEvent):
+ def __init__(self, target):
+ self.counter = 0
+ self.target = target
+ super().__init__(timeout=60)
+
+ def log_line_matches(self, line):
+ return 'Received a "run complete" message from worker' in line
+
+ def set(self):
+ self.counter += 1
+ if self.counter == self.target:
+ super().set()
+
+
+def _test_repo_internal(server_and_workers, test_repo, client, numof_runs):
+ server, workers = server_and_workers
+ event = LoggingEventRunComplete(numof_runs)
+ server.logger.add_event(event)
+ for i in range(numof_runs):
+ client.run('run', test_repo.path, 'HEAD')
+ event.wait()
+ assert numof_runs == test_repo.count_ci_output_files()
+
+
+def test_repo(server_and_workers, test_repo, client):
+ _test_repo_internal(server_and_workers, test_repo, client, 1)
+
+
+def test_repo_10(server_and_workers, test_repo, client):
+ _test_repo_internal(server_and_workers, test_repo, client, 10)